Passed
Push — develop ( 754fa3...6370fb )
by nguereza
02:20
created

AuthenticatorData::getAttestedCredentialData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Webauth
5
 *
6
 * Platine Webauthn is the implementation of webauthn specifications
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Webauth
11
 * Copyright (c) Jakob Bennemann <[email protected]>
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
declare(strict_types=1);
33
34
namespace Platine\Webauthn\Attestation;
35
36
use JsonSerializable;
37
use Platine\Webauthn\Entity\AttestedCredentialData;
38
use Platine\Webauthn\Entity\CredentialPublicKey;
39
use Platine\Webauthn\Entity\Flag;
40
use Platine\Webauthn\Exception\WebauthnException;
41
use Platine\Webauthn\Helper\CborDecoder;
42
43
/**
44
 * @class AuthenticatorData
45
 * @package Platine\Webauthn\Attestation
46
 */
47
class AuthenticatorData implements JsonSerializable
48
{
49
    public const EC2_TYPE = 2;
50
    public const RSA_TYPE = 3;
51
52
    /**
53
     * The binary raw data
54
     * @var string
55
     */
56
    protected string $binary;
57
58
    /**
59
     * The relying party ID hash
60
     * @var string
61
     */
62
    protected string $relyingPartyIdHash;
63
64
    /**
65
     * The authenticator data flag
66
     * @var Flag
67
     */
68
    protected Flag $flag;
69
70
    /**
71
     * The extension data
72
     * @var array<string, mixed>
73
     */
74
    protected array $extensionData = [];
75
76
    /**
77
     * The signature count
78
     * @var int
79
     */
80
    protected int $signatureCount = 0;
81
82
    /**
83
     * The attested credential data
84
     * @var AttestedCredentialData|null
85
     */
86
    protected ?AttestedCredentialData $attestedCredentialData = null;
87
88
    /**
89
     * Create new instance
90
     * @param string $binary
91
     */
92
    public function __construct(string $binary)
93
    {
94
        if (strlen($binary) < 37) {
95
            throw new WebauthnException('Invalid authenticator data provided');
96
        }
97
98
        $this->binary = $binary;
99
100
        // Read infos from binary
101
        // https://www.w3.org/TR/webauthn/#sec-authenticator-data
102
103
        $this->relyingPartyIdHash = substr($binary, 0, 32);
104
105
        // flags (1 byte)
106
        $this->createFlags();
107
108
        // signature counter: 32-bit unsigned big-endian integer.
109
        $signatureCount = unpack('Nsigncount', substr($this->binary, 33, 4));
110
        if ($signatureCount === false) {
111
            throw new WebauthnException('Can not unpack signature counter data');
112
        }
113
114
        $this->signatureCount = (int) $signatureCount['signcount'];
115
116
        $offset = 37;
117
        // https://www.w3.org/TR/webauthn/#sec-attested-credential-data
118
        if ($this->flag->isAttestedDataIncluded()) {
119
            $this->createAttestedData($offset);
120
        }
121
122
        if ($this->flag->isExtensionDataIncluded()) {
123
            $this->createExtensionData($offset);
124
        }
125
    }
126
127
    /**
128
     * Authenticator Attestation Globally Unique Identifier (AAGUID), a unique number
129
     * that identifies the model of the authenticator (not the specific instance
130
     * of the authenticator)
131
     * The AAGUID may be 0 if the user is using a old u2f device and/or if
132
     * the browser is using the FIDO-U2F format.
133
     *
134
     * @return string
135
     */
136
    public function getAaguid(): string
137
    {
138
        if ($this->attestedCredentialData === null) {
139
            throw new WebauthnException('Credential data not included in authenticator data');
140
        }
141
142
        return $this->attestedCredentialData->getAaguid();
143
    }
144
145
    /**
146
     *
147
     * @return string
148
     */
149
    public function getRelyingPartyIdHash(): string
150
    {
151
        return $this->relyingPartyIdHash;
152
    }
153
154
    /**
155
     *
156
     * @return int
157
     */
158
    public function getSignatureCount(): int
159
    {
160
        return $this->signatureCount;
161
    }
162
163
    /**
164
     *
165
     * @return bool
166
     */
167
    public function isUserPresent(): bool
168
    {
169
        return $this->flag->isUserPresent();
170
    }
171
172
    /**
173
     *
174
     * @return bool
175
     */
176
    public function isUserVerified(): bool
177
    {
178
        return $this->flag->isUserVerified();
179
    }
180
181
182
    /**
183
     * Return the public key in U2F format
184
     * @return string
185
     */
186
    public function getPublicKeyU2F(): string
187
    {
188
        if ($this->attestedCredentialData === null) {
189
            throw new WebauthnException('Credential data not included in authenticator data');
190
        }
191
192
        return "\x04" // ECC uncompressed
193
            . sprintf(
194
                '%s%s',
195
                $this->attestedCredentialData->getCredentialPublicKey()->getX(),
196
                $this->attestedCredentialData->getCredentialPublicKey()->getY()
197
            );
198
    }
199
200
    /**
201
     * Return the public key in PEM format
202
     * @return string
203
     */
204
    public function getPublicKeyPEM(): string
205
    {
206
        if ($this->attestedCredentialData === null) {
207
            throw new WebauthnException('Credential data not included in authenticator data');
208
        }
209
210
        // Distinguished Encoding Rules (DER)
211
        $der = null;
212
        $kty = $this->attestedCredentialData->getCredentialPublicKey()->getKty();
213
        switch ($kty) {
214
            case self::EC2_TYPE:
215
                $der = $this->getEC2DER();
216
                break;
217
            case self::RSA_TYPE:
218
                $der = $this->getRSADER();
219
                break;
220
            default:
221
                throw new WebauthnException(sprintf('Invalid key type [%d]', $kty));
222
        }
223
224
        $pem = '-----BEGIN PUBLIC KEY-----' . "\n";
225
        $pem .= chunk_split(base64_encode($der), 64, "\n");
226
        $pem .= '-----END PUBLIC KEY-----' . "\n";
227
228
        return $pem;
229
    }
230
231
    /**
232
     * Return the credential ID
233
     * @return string
234
     */
235
    public function getCredentialId(): string
236
    {
237
        if ($this->attestedCredentialData === null) {
238
            throw new WebauthnException('Credential data not included in authenticator data');
239
        }
240
241
        return $this->attestedCredentialData->getCredentialId();
242
    }
243
244
    /**
245
     * Return the binary data
246
     * @return string
247
     */
248
    public function getBinary(): string
249
    {
250
        return $this->binary;
251
    }
252
253
    /**
254
     *
255
     * @return array<string, mixed>
256
     */
257
    public function getExtensionData(): array
258
    {
259
        return $this->extensionData;
260
    }
261
262
    /**
263
     *
264
     * @return AttestedCredentialData|null
265
     */
266
    public function getAttestedCredentialData(): ?AttestedCredentialData
267
    {
268
        return $this->attestedCredentialData;
269
    }
270
271
272
    /**
273
    * {@inheritdoc}
274
    * @return mixed
275
    */
276
    public function jsonSerialize()
277
    {
278
        return get_object_vars($this);
279
    }
280
281
    /**
282
     * Return the EC2 Distinguished Encoding Rules (DER)
283
     * @return string
284
     */
285
    protected function getEC2DER(): string
286
    {
287
        return $this->getDERSequence(
288
            $this->getDERSequence(
289
                $this->getDEROid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey
290
                $this->getDEROid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1
291
            ) .
292
            $this->getDERBitString($this->getPublicKeyU2F())
293
        );
294
    }
295
296
    /**
297
     * Return the RSA Distinguished Encoding Rules (DER)
298
     * @return string
299
     */
300
    protected function getRSADER(): string
301
    {
302
        if ($this->attestedCredentialData === null) {
303
            throw new WebauthnException('Credential data not included in authenticator data');
304
        }
305
306
        return $this->getDERSequence(
307
            $this->getDERSequence(
308
                $this->getDEROid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption
309
                $this->getDERNullValue()
310
            ) .
311
            $this->getDERBitString(
312
                $this->getDERSequence(
313
                    $this->getDERUnsignedInteger(
314
                        (string) $this->attestedCredentialData->getCredentialPublicKey()->getN()
315
                    ) .
316
                    $this->getDERUnsignedInteger(
317
                        (string) $this->attestedCredentialData->getCredentialPublicKey()->getE()
318
                    )
319
                )
320
            )
321
        );
322
    }
323
324
    /**
325
     * Create the extension data
326
     * @param int $offset
327
     * @return void
328
     */
329
    protected function createExtensionData(int &$offset): void
330
    {
331
        $data = substr($this->binary, $offset);
332
333
        $extensionData = CborDecoder::decode($data);
334
335
        if (! is_array($extensionData)) {
336
            throw new WebauthnException('Invalid extension data');
337
        }
338
339
        $this->extensionData = $extensionData;
340
    }
341
342
    /**
343
     * Create the attested data
344
     * @param int $offset
345
     * @return void
346
     */
347
    protected function createAttestedData(int &$offset): void
348
    {
349
        $attestedData = new AttestedCredentialData($this->binary);
350
351
        // set end offset
352
        $offset = 55 + $attestedData->getLength();
353
354
        // Create credential public key
355
        $credentialPublicKey = new CredentialPublicKey(
356
            $this->binary,
357
            55 + $attestedData->getLength(),
358
            $offset
359
        );
360
        $attestedData->setCredentialPublicKey($credentialPublicKey);
361
362
        $this->attestedCredentialData = $attestedData;
363
    }
364
365
    /**
366
     * Create flags
367
     * @return void
368
     */
369
    protected function createFlags(): void
370
    {
371
        $flags = unpack('Cflags', substr($this->binary, 32, 1));
372
        if ($flags === false) {
373
            throw new WebauthnException('Can not unpack flags data');
374
        }
375
376
        $this->flag = new Flag((int) $flags['flags']);
377
    }
378
379
    /**
380
     * Return Distinguished Encoding Rules (DER) length
381
     * @param int $length
382
     * @return string
383
     */
384
    protected function getDERLength(int $length): string
385
    {
386
        if ($length < 128) {
387
            return chr($length);
388
        }
389
390
        $byteLength = '';
391
        while ($length > 0) {
392
            $byteLength = chr($length % 256) . $byteLength;
393
394
            $length = intdiv($length, 256);
395
        }
396
397
        return chr(0x80 | strlen($byteLength)) . $byteLength;
398
    }
399
400
    /**
401
     * Return Distinguished Encoding Rules (DER) OID
402
     * @param string $encoded
403
     * @return string
404
     */
405
    protected function getDEROid(string $encoded): string
406
    {
407
        return "\x06" . $this->getDERLength(strlen($encoded)) . $encoded;
408
    }
409
410
    /**
411
     * Return Distinguished Encoding Rules (DER) sequence
412
     * @param string $contents
413
     * @return string
414
     */
415
    protected function getDERSequence(string $contents): string
416
    {
417
        return "\x30" . $this->getDERLength(strlen($contents)) . $contents;
418
    }
419
420
    /**
421
     * Return Distinguished Encoding Rules (DER) bit string
422
     * @param string $bytes
423
     * @return string
424
     */
425
    protected function getDERBitString(string $bytes): string
426
    {
427
        return "\x03" . $this->getDERLength(strlen($bytes) + 1) . "\x00" . $bytes;
428
    }
429
430
    /**
431
     * Return Distinguished Encoding Rules (DER) null value
432
     * @return string
433
     */
434
    protected function getDERNullValue(): string
435
    {
436
        return "\x05\x00";
437
    }
438
439
    /**
440
     * Return Distinguished Encoding Rules (DER) unsigned integer
441
     * @param string $bytes
442
     * @return string
443
     */
444
    protected function getDERUnsignedInteger(string $bytes): string
445
    {
446
        $length = strlen($bytes);
447
        // Remove leading zero bytes
448
        for ($i = 0; $i < ($length - 1); $i++) {
449
            if (ord($bytes[$i]) !== 0) {
450
                break;
451
            }
452
        }
453
454
        if ($i !== 0) {
455
            $bytes = substr($bytes, $i);
456
        }
457
458
        // If most significant bit is set, prefix with another zero
459
        // to prevent it being seen as negative number
460
        if ((ord($bytes[0]) & 0x80) !== 0) {
461
            $bytes = "\x00" . $bytes;
462
        }
463
464
        return "\x02" . $this->getDERLength(strlen($bytes)) . $bytes;
465
    }
466
}
467