Passed
Push — develop ( 548a36...8ad7fd )
by nguereza
01:55
created

AuthenticatorData::getRelyingPartyIdHash()   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
    * {@inheritdoc}
255
    * @return mixed
256
    */
257
    public function jsonSerialize()
258
    {
259
        return get_object_vars($this);
260
    }
261
262
    /**
263
     * Return the EC2 Distinguished Encoding Rules (DER)
264
     * @return string
265
     */
266
    protected function getEC2DER(): string
267
    {
268
        return $this->getDERSequence(
269
            $this->getDERSequence(
270
                $this->getDEROid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey
271
                $this->getDEROid("\x2A\x86\x48\xCE\x3D\x03\x01\x07") // 1.2.840.10045.3.1.7 prime256v1
272
            ) .
273
            $this->getDERBitString($this->getPublicKeyU2F())
274
        );
275
    }
276
277
    /**
278
     * Return the RSA Distinguished Encoding Rules (DER)
279
     * @return string
280
     */
281
    protected function getRSADER(): string
282
    {
283
        if ($this->attestedCredentialData === null) {
284
            throw new WebauthnException('Credential data not included in authenticator data');
285
        }
286
287
        return $this->getDERSequence(
288
            $this->getDERSequence(
289
                $this->getDEROid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption
290
                $this->getDERNullValue()
291
            ) .
292
            $this->getDERBitString(
293
                $this->getDERSequence(
294
                    $this->getDERUnsignedInteger(
295
                        (string) $this->attestedCredentialData->getCredentialPublicKey()->getN()
296
                    ) .
297
                    $this->getDERUnsignedInteger(
298
                        (string) $this->attestedCredentialData->getCredentialPublicKey()->getE()
299
                    )
300
                )
301
            )
302
        );
303
    }
304
305
    /**
306
     * Create the extension data
307
     * @param int $offset
308
     * @return void
309
     */
310
    protected function createExtensionData(int &$offset): void
311
    {
312
        $data = substr($this->binary, $offset);
313
314
        $extensionData = CborDecoder::decode($data);
315
316
        if (! is_array($extensionData)) {
317
            throw new WebauthnException('Invalid extension data');
318
        }
319
320
        $this->extensionData = $extensionData;
321
    }
322
323
    /**
324
     * Create the attested data
325
     * @param int $offset
326
     * @return void
327
     */
328
    protected function createAttestedData(int &$offset): void
329
    {
330
        $attestedData = new AttestedCredentialData($this->binary);
331
332
        // set end offset
333
        $offset = 55 + $attestedData->getLength();
334
335
        // Create credential public key
336
        $credentialPublicKey = new CredentialPublicKey(
337
            $this->binary,
338
            55 + $attestedData->getLength(),
339
            $offset
340
        );
341
        $attestedData->setCredentialPublicKey($credentialPublicKey);
342
343
        $this->attestedCredentialData = $attestedData;
344
    }
345
346
    /**
347
     * Create flags
348
     * @return void
349
     */
350
    protected function createFlags(): void
351
    {
352
        $flags = unpack('Cflags', substr($this->binary, 32, 1));
353
        if ($flags === false) {
354
            throw new WebauthnException('Can not unpack flags data');
355
        }
356
357
        $this->flag = new Flag((int) $flags['flags']);
358
    }
359
360
    /**
361
     * Return Distinguished Encoding Rules (DER) length
362
     * @param int $length
363
     * @return string
364
     */
365
    protected function getDERLength(int $length): string
366
    {
367
        if ($length < 128) {
368
            return chr($length);
369
        }
370
371
        $byteLength = '';
372
        while ($length > 0) {
373
            $byteLength = chr($length % 256) . $byteLength;
374
375
            $length = intdiv($length, 256);
376
        }
377
378
        return chr(0x80 | strlen($byteLength)) . $byteLength;
379
    }
380
381
    /**
382
     * Return Distinguished Encoding Rules (DER) OID
383
     * @param string $encoded
384
     * @return string
385
     */
386
    protected function getDEROid(string $encoded): string
387
    {
388
        return "\x06" . $this->getDERLength(strlen($encoded)) . $encoded;
389
    }
390
391
    /**
392
     * Return Distinguished Encoding Rules (DER) sequence
393
     * @param string $contents
394
     * @return string
395
     */
396
    protected function getDERSequence(string $contents): string
397
    {
398
        return "\x30" . $this->getDERLength(strlen($contents)) . $contents;
399
    }
400
401
    /**
402
     * Return Distinguished Encoding Rules (DER) bit string
403
     * @param string $bytes
404
     * @return string
405
     */
406
    protected function getDERBitString(string $bytes): string
407
    {
408
        return "\x03" . $this->getDERLength(strlen($bytes) + 1) . "\x00" . $bytes;
409
    }
410
411
    /**
412
     * Return Distinguished Encoding Rules (DER) null value
413
     * @return string
414
     */
415
    protected function getDERNullValue(): string
416
    {
417
        return "\x05\x00";
418
    }
419
420
    /**
421
     * Return Distinguished Encoding Rules (DER) unsigned integer
422
     * @param string $bytes
423
     * @return string
424
     */
425
    protected function getDERUnsignedInteger(string $bytes): string
426
    {
427
        $length = strlen($bytes);
428
        // Remove leading zero bytes
429
        for ($i = 0; $i < ($length - 1); $i++) {
430
            if (ord($bytes[$i]) !== 0) {
431
                break;
432
            }
433
        }
434
435
        if ($i !== 0) {
436
            $bytes = substr($bytes, $i);
437
        }
438
        
439
        // If most significant bit is set, prefix with another zero
440
        // to prevent it being seen as negative number
441
        if ((ord($bytes[0]) & 0x80) !== 0) {
442
            $bytes = "\x00" . $bytes;
443
        }
444
445
        return "\x02" . $this->getDERLength(strlen($bytes)) . $bytes;
446
    }
447
}
448