Passed
Push — master ( 74c929...60a713 )
by Stefan
10:18
created

WebAuthnRegistrationEvent::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 1
eloc 9
nc 1
nop 8
dl 0
loc 27
rs 9.9666
c 5
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace SimpleSAML\Module\webauthn\WebAuthn;
4
5
use Cose\Key\Ec2Key;
6
use SimpleSAML\Logger;
7
use SimpleSAML\Utils;
8
9
/**
10
 * FIDO2/WebAuthn Authentication Processing filter
11
 *
12
 * Filter for registering or authenticating with a FIDO2/WebAuthn token after
13
 * having authenticated with the primary authsource.
14
 *
15
 * @package SimpleSAMLphp
16
 */
17
class WebAuthnRegistrationEvent extends WebAuthnAbstractEvent
18
{
19
    /**
20
     * Public key algorithm supported. This is -7 - ECDSA with curve P-256
21
     */
22
    public const PK_ALGORITHM = "-7";
23
    public const AAGUID_ASSURANCE_LEVEL_NONE = 0;
24
    public const AAGUID_ASSURANCE_LEVEL_SELF = 1;
25
    public const AAGUID_ASSURANCE_LEVEL_BASIC = 2;
26
    public const AAGUID_ASSURANCE_LEVEL_ATTCA = 3;
27
28
    /**
29
     * the AAGUID of the newly registered authenticator
30
     * @var string
31
     */
32
    protected $AAGUID;
33
34
    /**
35
     * how sure are we about the AAGUID?
36
     * @var int
37
     */
38
    protected $AAGUIDAssurance;
39
40
    /**
41
     * An array of known hardware tokens
42
     *
43
     * @var AAGUID
44
     */
45
    protected $AAGUIDDictionary;
46
47
    /**
48
     * Initialize the event object.
49
     *
50
     * Validates and parses the configuration.
51
     *
52
     * @param string $pubkeyCredType  PublicKeyCredential.type
53
     * @param string $scope           the scope of the event
54
     * @param string $challenge       the challenge which was used to trigger this event
55
     * @param string $idpEntityId     the entity ID of our IdP
56
     * @param string $attestationData the attestation data CBOR blob
57
     * @param string $responseId      the response ID
58
     * @param string $clientDataJSON  the client data JSON string which is present in all types of events
59
     * @param bool $debugMode         print debugging statements?
60
     */
61
    public function __construct(
62
        string $pubkeyCredType,
63
        string $scope,
64
        string $challenge,
65
        string $idpEntityId,
66
        string $attestationData,
67
        string $responseId,
68
        string $clientDataJSON,
69
        bool $debugMode = false
70
    ) {
71
        $this->debugBuffer .= "attestationData raw: " . $attestationData . "<br/>";
72
        /**
73
         * §7.1 STEP 9 : CBOR decode attestationData.
74
         */
75
        $attestationArray = $this->cborDecode($attestationData);
76
        $authData = $attestationArray['authData'];
77
        $this->eventType = "REG";
78
        parent::__construct($pubkeyCredType, $scope, $challenge, $idpEntityId, $authData, $clientDataJSON, $debugMode);
79
80
        $this->AAGUIDDictionary = AAGUID::getInstance();
81
82
        // this function extracts the public key
83
        $this->validateAttestedCredentialData(substr($authData, 37), $responseId);
84
        // this function may need the public key to have been previously extracted
85
        $this->validateAttestationData($attestationData);
86
        // the following function sets the credential properties
87
        $this->debugBuffer .= "Attestation Data (bin2hex): " . bin2hex(substr($authData, 37)) . "<br/>";
88
    }
89
90
91
    /**
92
     * Validate the incoming attestation data CBOR blob and return the embedded authData
93
     * @param string $attestationData
94
     */
95
    private function validateAttestationData(string $attestationData): void
96
    {
97
        /**
98
         * STEP 9 of the validation procedure in § 7.1 of the spec: CBOR-decode the attestationObject
99
         */
100
        $attestationArray = $this->cborDecode($attestationData);
101
        $this->debugBuffer .= "<pre>";
102
        $this->debugBuffer .= print_r($attestationArray, true);
103
        $this->debugBuffer .= "</pre>";
104
105
        /**
106
         * STEP 15 of the validation procedure in § 7.1 of the spec: verify attStmt values
107
         */
108
        switch ($attestationArray['fmt']) {
109
            case "none":
110
                $this->validateAttestationFormatNone($attestationArray);
111
                break;
112
            case "packed":
113
                $this->validateAttestationFormatPacked($attestationArray);
114
                break;
115
            case "fido-u2f":
116
                $this->validateAttestationFormatFidoU2F($attestationArray);
117
                break;
118
            case "android-safetynet":
119
                $this->validateAttestationFormatAndroidSafetyNet($attestationArray);
120
                break;
121
	    case "apple":
122
		$this->validateAttestationFormatApple($attestationArray);
123
		break;
124
            case "tpm":
125
            case "android-key":
126
                $this->fail("Attestation format " . $attestationArray['fmt'] . " validation not supported right now.");
127
                break;
128
            default:
129
                $this->fail("Unknown attestation format.");
130
                break;
131
        }
132
    }
133
134
135
    /**
136
     * @param array $attestationArray
137
     */
138
    private function validateAttestationFormatNone(array $attestationArray): void
139
    {
140
        // § 8.7 of the spec
141
        /**
142
         * § 7.1 Step 16 && §8.7 Verification Procedure: stmt must be an empty array
143
         * § 7.1 Step 17+18 are a NOOP if the format was "none" (which is acceptable as per this RPs policy)
144
         */
145
        if (count($attestationArray['attStmt']) === 0) {
146
            $this->pass("Attestation format and statement as expected, and no attestation authorities to retrieve.");
147
            $this->AAGUIDAssurance = self::AAGUID_ASSURANCE_LEVEL_NONE;
148
            return;
149
        } else {
150
            $this->fail("Non-empty attestation authorities are not expected with 'attestationFormat = none'.");
151
        }
152
    }
153
154
155
    /**
156
     * @param array $attestationArray
157
     */
158
    private function validateAttestationFormatApple(array $attestationArray): void
159
    {
160
161
	// found at: https://www.apple.com/certificateauthority/private/
162
163
	$APPLE_WEBAUTHN_ROOT_CA = "-----BEGIN CERTIFICATE-----
164
MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
165
HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
166
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
167
NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
168
A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
169
AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
170
xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
171
pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
172
2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
173
MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
174
jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
175
1bWeT0vT
176
-----END CERTIFICATE-----";
177
        // § 8.8 Bullet 1 of the draft spec at https://pr-preview.s3.amazonaws.com/alanwaketan/webauthn/pull/1491.html#sctn-apple-anonymous-attestation
178
	// draft implemented in state of 11 Feb 2021
179
180
	// I can't help but notice that the verification procedure does NOTHING with CA certs from the chain, nor is there a root to validate to!
181
	// Found the root CA with Google, see above, and will perform chain validation even if the spec doesn't say so.
182
183
	// first, clear the openssl error backlog. We might need error data in case things go sideways.
184
	while(openssl_error_string() !== false);
185
186
        $stmtDecoded = $attestationArray['attStmt'];
187
	if (!isset($stmtDecoded['x5c'])) {
188
		$this->fail("Apple attestation statement does not contain an x5c attestation statement!");
189
	}
190
	// § 8.8 Bullet 2
191
        $nonceToHash = $attestationArray['authData'] . $this->clientDataHash;
192
	// § 8.8 Bullet 3
193
	$nonce = hash("sha256", $nonceToHash, TRUE); // does raw_output have to be FALSE or TRUE?
194
        $certProps = openssl_x509_parse(Utils\Crypto::der2pem($stmtDecoded['x5c'][0]));	
195
	// § 8.8 Bullet 4
196
        if (
197
           !isset($certProps['extensions']['1.2.840.113635.100.8.2'])
198
           || empty($certProps['extensions']['1.2.840.113635.100.8.2'])
199
                ) {
200
                    $this->fail( "The required nonce value is not present in the OID." );
201
                }
202
	$toCompare = substr($certProps['extensions']['1.2.840.113635.100.8.2'], 6);
203
	if ($nonce != $toCompare) {
204
		$this->fail("There is a mismatch between the nonce and the OID (XXX $nonce XXX , XXX $toCompare XXX ).");
205
	}
206
207
	// chain validation first
208
	foreach ( $stmtDecoded['x5c'] as $runIndex => $runCert ) {
209
		if (isset($stmtDecoded['x5c'][$runIndex + 1])) { // there is a next cert, so follow the chain
210
			$certResource = openssl_x509_read(Utils\Crypto::der2pem($runCert));
211
			$signerPubKey = openssl_pkey_get_public(Utils\Crypto::der2pem($stmtDecoded['x5c'][$runIndex + 1]));
212
			if (openssl_x509_verify($certResource, $signerPubKey) != 1) {
213
				$this->fail("Error during chain validation of the attestation certificate (while validating cert #$runIndex, which is "
214
                                    . Utils\Crypto::der2pem($runCert)
215
                                    . "; next cert was "
216
                                    . Utils\Crypto::der2pem($stmtDecoded['x5c'][$runIndex + 1]));
217
			}
218
		} else { // last cert, compare to the root
219
			$certResource = openssl_x509_read(Utils\Crypto::der2pem($runCert));
220
			$signerPubKey = openssl_pkey_get_public($APPLE_WEBAUTHN_ROOT_CA);
221
			if (openssl_x509_verify($certResource, $signerPubKey) != 1) {
222
                                $this->fail("Error during root CA validation of the attestation chain certificate, which is ".Utils\Crypto::der2pem($runCert));
223
                        }
224
		}
225
	}
226
227
        $keyResource = openssl_pkey_get_public(Utils\Crypto::der2pem($stmtDecoded['x5c'][0]));
228
        if ($keyResource === FALSE) {
229
		$this->fail("Did not get a parseable X.509 structure out of the Apple attestation statement - x5c nr. 0 statement was: XXX "
230
                    . $stmtDecoded['x5c'][0]
231
                    . " XXX; PEM equivalent is "
232
                    . Utils\Crypto::der2pem($stmtDecoded['x5c'][0])
233
                    . ". OpenSSL error: "
234
                    . openssl_error_string()
235
                    );
236
	}
237
	// $this->credential is a public key in CBOR, not "PEM". We need to convert it first.
238
        $keyArray = $this->cborDecode(hex2bin($this->credential));
239
        $keyObject = new Ec2Key($keyArray);
240
        $credentialResource = openssl_pkey_get_public($keyObject->asPEM());
241
242
        if ($credentialResource === FALSE) {
243
                $this->fail("Could not create a public key from CBOR credential. XXX "
244
                    . $this->credential
245
                    . " XXX; PEM equivalent is "
246
                    . $keyObject->asPEM()
247
                    . ". OpenSSL error: "
248
                    . openssl_error_string()
249
                    );
250
        }
251
	// § 8.8 Bullet 5
252
	$credentialDetails = openssl_pkey_get_details($credentialResource);
253
	$keyDetails = openssl_pkey_get_details($keyResource);
254
	if ( $credentialDetails['bits'] != $keyDetails['bits'] ||
255
             $credentialDetails['key']  != $keyDetails['key']  ||
256
             $credentialDetails['type'] != $keyDetails['type'] ) { 
257
		$this->fail("The credential public key does not match the certificate public key in attestationData. ("
258
              . $credentialDetails['key'] 
259
              . " - "
260
              . $keyDetails['key'] 
261
              . ")");
262
	}
263
	$this->pass("Apple attestation format verification passed.");
264
	return;
265
    }
266
267
    /**
268
     * @param array $attestationArray
269
     */
270
    private function validateAttestationFormatPacked(array $attestationArray): void
271
    {
272
        $stmtDecoded = $attestationArray['attStmt'];
273
        $this->debugBuffer .= "AttStmt: " . print_r($stmtDecoded, true) . "<br/>";
0 ignored issues
show
Bug introduced by
Are you sure print_r($stmtDecoded, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

273
        $this->debugBuffer .= "AttStmt: " . /** @scrutinizer ignore-type */ print_r($stmtDecoded, true) . "<br/>";
Loading history...
274
        /**
275
         * §7.1 Step 16: attestation is either done with x5c or ecdaa.
276
         */
277
        if (isset($stmtDecoded['x5c'])) {
278
            $this->validateAttestationFormatPackedX5C($attestationArray);
279
        } elseif (isset($stmtDecoded['ecdaa'])) {
280
            $this->fail("ecdaa attestation not supported right now.");
281
        } else {
282
            // if we are still here, we are in the "self" type.
283
            $this->validateAttestationFormatPackedSelf($attestationArray);
284
        }
285
    }
286
287
288
    /**
289
     * @param array $attestationArray
290
     */
291
    private function validateAttestationFormatPackedX5C(array $attestationArray): void
292
    {
293
        $stmtDecoded = $attestationArray['attStmt'];
294
        /**
295
         * §8.2 Step 2: check x5c attestation
296
         */
297
        $sigdata = $attestationArray['authData'] . $this->clientDataHash;
298
        $keyResource = openssl_pkey_get_public(Utils\Crypto::der2pem($stmtDecoded['x5c'][0]));
299
        if ($keyResource === false) {
300
            $this->fail("Unable to construct public key resource from PEM.");
301
        }
302
        /**
303
         * §8.2 Step 2 Bullet 1: check signature
304
         */
305
        if (openssl_verify($sigdata, $stmtDecoded['sig'], $keyResource, OPENSSL_ALGO_SHA256) !== 1) {
306
            $this->fail("x5c attestation failed.");
307
        }
308
        $this->pass("x5c sig check passed.");
309
        // still need to perform sanity checks on the attestation certificate
310
        /**
311
         * §8.2 Step 2 Bullet 2: check certificate properties listed in §8.2.1
312
         */
313
        $certProps = openssl_x509_parse(Utils\Crypto::der2pem($stmtDecoded['x5c'][0]));
314
        $this->debugBuffer .= "Attestation Certificate:" . print_r($certProps, true) . "<br/>";
0 ignored issues
show
Bug introduced by
Are you sure print_r($certProps, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

314
        $this->debugBuffer .= "Attestation Certificate:" . /** @scrutinizer ignore-type */ print_r($certProps, true) . "<br/>";
Loading history...
315
        if (
316
            $certProps['version'] !== 2 || /** §8.2.1 Bullet 1 */
317
            $certProps['subject']['OU'] !== "Authenticator Attestation" || /** §8.2.1 Bullet 2 [Subject-OU] */
318
            !isset($certProps['subject']['CN']) || /** §8.2.1 Bullet 2 [Subject-CN] */
319
            !isset($certProps['extensions']['basicConstraints']) ||
320
            strstr("CA:FALSE", $certProps['extensions']['basicConstraints']) === false /** §8.2.1 Bullet 4 */
321
        ) {
322
            $this->fail("Attestation certificate properties are no good.");
323
        }
324
325
        if ($this->AAGUIDDictionary->hasToken($this->AAGUID)) {
326
            $token = $this->AAGUIDDictionary->get($this->AAGUID);
327
            if (
328
                $certProps['subject']['O'] !== $token['O'] ||
329
                // §8.2.1 Bullet 2 [Subject-O]
330
                $certProps['subject']['C'] !== $token['C']
331
                // §8.2ubject-C]
332
            ) {
333
                $this->fail("AAGUID does not match vendor data.");
334
            }
335
            if ($token['multi'] === true) { // need to check the OID
336
                if (
337
                    !isset($certProps['extensions']['1.3.6.1.4.1.45724.1.1.4'])
338
                    || empty($certProps['extensions']['1.3.6.1.4.1.45724.1.1.4'])
339
                ) { /** §8.2.1 Bullet 3 */
340
                    $this->fail(
341
                        "This vendor uses one cert for multiple authenticator model attestations, but lacks the AAGUID OID."
342
                    );
343
                }
344
                /**
345
                 * §8.2 Step 2 Bullet 3: compare AAGUID values
346
                 */
347
                $AAGUIDFromOid = substr(bin2hex($certProps['extensions']['1.3.6.1.4.1.45724.1.1.4']), 4);
348
                $this->debugBuffer .= "AAGUID from OID = $AAGUIDFromOid<br/>";
349
                if (strtolower($AAGUIDFromOid) !== strtolower($this->AAGUID)) {
350
                    $this->fail("AAGUID mismatch between attestation certificate and attestation statement.");
351
                }
352
            }
353
            // we would need to verify the attestation certificate against a known-good
354
            // root CA certificate to get more than basic
355
            /*
356
             * §7.1 Step 17 is to look at $token['RootPEMs']
357
             */
358
            /*
359
             * §7.1 Step 18 is skipped, and we unconditionally return "only" Basic.
360
             */
361
            $this->AAGUIDAssurance = self::AAGUID_ASSURANCE_LEVEL_BASIC;
362
        } else {
363
            $this->warn("Unknown authenticator model found: " . $this->AAGUID . ".");
364
            // unable to verify all cert properties, so this is not enough for BASIC.
365
            // but it's our own fault, we should add the device to our DB.
366
            $this->AAGUIDAssurance = self::AAGUID_ASSURANCE_LEVEL_SELF;
367
        }
368
        $this->pass("x5c attestation passed.");
369
        return;
370
    }
371
372
373
    /**
374
     * @param array $attestationArray
375
     */
376
    private function validateAttestationFormatPackedSelf(array $attestationArray): void
377
    {
378
        $stmtDecoded = $attestationArray['attStmt'];
379
        /**
380
         * §8.2 Step 4 Bullet 1: check algorithm
381
         */
382
        if ($stmtDecoded['alg'] !== self::PK_ALGORITHM) {
383
            $this->fail("Unexpected algorithm type in packed basic attestation: " . $stmtDecoded['alg'] . ".");
384
        }
385
        $keyObject = new Ec2Key($this->cborDecode(hex2bin($this->credential)));
386
        $keyResource = openssl_pkey_get_public($keyObject->asPEM());
387
        if ($keyResource === false) {
388
            $this->fail("Unable to construct public key resource from PEM.");
389
        }
390
        $sigdata = $attestationArray['authData'] . $this->clientDataHash;
391
        /**
392
         * §8.2 Step 4 Bullet 2: verify signature
393
         */
394
        if (openssl_verify($sigdata, $stmtDecoded['sig'], $keyResource, OPENSSL_ALGO_SHA256) === 1) {
395
            $this->pass("Self-Attestation veried.");
396
            /**
397
             * §8.2 Step 4 Bullet 3: return Self level
398
             */
399
            $this->AAGUIDAssurance = self::AAGUID_ASSURANCE_LEVEL_SELF;
400
        } else {
401
            $this->fail("Self-Attestation failed.");
402
        }
403
    }
404
405
406
    /**
407
     * support legacy U2F tokens
408
     *
409
     * @param array $attestationData the incoming attestation data
410
     */
411
    private function validateAttestationFormatFidoU2F(array $attestationData): void
412
    {
413
        /**
414
         * §8.6 Verification Step 1 is a NOOP: if we're here, the attStmt was
415
         * already successfully CBOR decoded
416
         */
417
        $stmtDecoded = $attestationData['attStmt'];
418
        if (!isset($stmtDecoded['x5c'])) {
419
            $this->fail("FIDO U2F attestation needs to have the 'x5c' key");
420
        }
421
        /**
422
         * §8.6 Verification Step 2: extract attCert and sanity check it
423
         */
424
        if (count($stmtDecoded['x5c']) !== 1) {
425
            $this->fail("FIDO U2F attestation requires 'x5c' to have only exactly one key.");
426
        }
427
        $attCert = Utils\Crypto::der2pem($stmtDecoded['x5c'][0]);
428
        $key = openssl_pkey_get_public($attCert);
429
        $keyProps = openssl_pkey_get_details($key);
430
        if (!isset($keyProps['ec']['curve_name']) || $keyProps['ec']['curve_name'] !== "prime256v1") {
431
            $this->fail("FIDO U2F attestation public key is not P-256!");
432
        }
433
        /**
434
         * §8.6 Verification Step 3 is a NOOP as these properties are already
435
         * available as class members:
436
         *
437
         * $this->rpIdHash;
438
         * $this->credentialId;
439
         * $this->credential;
440
         */
441
        /**
442
         * §8.6 Verification Step 4: encode the public key in ANSI X9.62 format
443
         */
444
        if (
445
            isset($this->credential[-2]) &&
446
            strlen($this->credential[-2]) === 32 &&
447
            isset($this->credential[-3]) &&
448
            strlen($this->credential[-3]) === 32
449
        ) {
450
            $publicKeyU2F = chr(4) . $this->credential[-2] . $this->credential[-3];
451
        } else {
452
            $publicKeyU2F = false;
453
            $this->fail("FIDO U2F attestation: the public key is not as expected.");
454
        }
455
        /**
456
         * §8.6 Verification Step 5: create verificationData
457
         *
458
         * @psalm-var string $publicKeyU2F
459
         */
460
        $verificationData = chr(0) . $this->rpIdHash . $this->clientDataHash . $this->credentialId . $publicKeyU2F;
461
        /**
462
         * §8.6 Verification Step 6: verify signature
463
         */
464
        if (openssl_verify($verificationData, $stmtDecoded['sig'], $attCert, OPENSSL_ALGO_SHA256) !== 1) {
465
            $this->fail("FIDO U2F Attestation verification failed.");
466
        } else {
467
            $this->pass("Successfully verified FIDO U2F signature.");
468
        }
469
        /**
470
         * §8.6 Verification Step 7: not performed, this is optional as per spec
471
         */
472
        /**
473
         * §8.6 Verification Step 8: so we always settle for "Basic"
474
         */
475
        $this->AAGUIDAssurance = self::AAGUID_ASSURANCE_LEVEL_BASIC;
476
    }
477
478
479
    /**
480
     * support Android authenticators (fingerprint etc.)
481
     *
482
     * @param array $attestationData the incoming attestation data
483
     */
484
    private function validateAttestationFormatAndroidSafetyNet(array $attestationData): void
0 ignored issues
show
Unused Code introduced by
The parameter $attestationData is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

484
    private function validateAttestationFormatAndroidSafetyNet(/** @scrutinizer ignore-unused */ array $attestationData): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
485
    {
486
    }
487
488
489
    /**
490
     * The registration contains the actual credential. This function parses it.
491
     * @param string $attData    the attestation data binary blob
492
     * @param string $responseId the response ID
493
     */
494
    private function validateAttestedCredentialData(string $attData, string $responseId): void
495
    {
496
        $aaguid = substr($attData, 0, 16);
497
        $credIdLenBytes = substr($attData, 16, 2);
498
        $credIdLen = intval(bin2hex($credIdLenBytes), 16);
499
        $credId = substr($attData, 18, $credIdLen);
500
        $this->debugBuffer .= "AAGUID (hex) = " . bin2hex($aaguid) . "</br/>";
501
        $this->AAGUID = bin2hex($aaguid);
502
        $this->debugBuffer .= "Length Raw = " . bin2hex($credIdLenBytes) . "<br/>";
503
        $this->debugBuffer .= "Credential ID Length (decimal) = " . $credIdLen . "<br/>";
504
        $this->debugBuffer .= "Credential ID (hex) = " . bin2hex($credId) . "<br/>";
505
        if (bin2hex(WebAuthnAbstractEvent::base64urlDecode($responseId)) === bin2hex($credId)) {
506
            $this->pass("Credential IDs in authenticator response and in attestation data match.");
507
        } else {
508
            $this->fail(
509
                "Mismatch of credentialId (" . bin2hex($credId) . ") vs. response ID (" .
510
                bin2hex(WebAuthnAbstractEvent::base64urlDecode($responseId)) . ")."
511
            );
512
        }
513
        // so far so good. Now extract the actual public key from its COSE
514
        // encoding.
515
        // finding out the number of bytes to CBOR decode appears non-trivial.
516
        // The simple case is if no ED is present as the CBOR data then goes to
517
        // the end of the byte sequence.
518
        // Since we made sure above that no ED is in the sequence, take the rest
519
        // of the sequence in its entirety.
520
        $pubKeyCBOR = substr($attData, 18 + $credIdLen);
521
        $arrayPK = $this->cborDecode($pubKeyCBOR);
522
        $this->debugBuffer .= "pubKey in canonical form: <pre>" . print_r($arrayPK, true) . "</pre>";
0 ignored issues
show
Bug introduced by
Are you sure print_r($arrayPK, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

522
        $this->debugBuffer .= "pubKey in canonical form: <pre>" . /** @scrutinizer ignore-type */ print_r($arrayPK, true) . "</pre>";
Loading history...
523
        /**
524
         * STEP 13 of the validation procedure in § 7.1 of the spec: is the algorithm the expected one?
525
         */
526
        if ($arrayPK['3'] === self::PK_ALGORITHM) { // we requested -7, so want to see it here
527
            $this->pass("Public Key Algorithm is the expected one (-7, ECDSA).");
528
        } else {
529
            $this->fail("Public Key Algorithm mismatch!");
530
        }
531
        $this->credentialId = bin2hex($credId);
532
        $this->credential = bin2hex($pubKeyCBOR);
533
    }
534
535
536
    /**
537
     * @return string
538
     */
539
    public function getAAGUID(): string
540
    {
541
        return $this->AAGUID;
542
    }
543
}
544