Passed
Pull Request — master (#39)
by Tim
03:06
created

validateAttestationFormatApple()   F

Complexity

Conditions 15
Paths 640

Size

Total Lines 107
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 59
c 1
b 0
f 0
dl 0
loc 107
rs 2.25
cc 15
nc 640
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SimpleSAML\Module\webauthn\WebAuthn;
4
5
use Cose\Key\Ec2Key;
6
use SimpleSAML\Logger;
7
use SimpleSAML\Utils\Config as SSPConfig;
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
 * @author Stefan Winter <[email protected]>
16
 * @package SimpleSAMLphp
17
 */
18
class WebAuthnRegistrationEvent extends WebAuthnAbstractEvent
19
{
20
    /**
21
     * Public key algorithm supported. This is -7 - ECDSA with curve P-256
22
     */
23
    public const PK_ALGORITHM = "-7";
24
    public const AAGUID_ASSURANCE_LEVEL_NONE = 0;
25
    public const AAGUID_ASSURANCE_LEVEL_SELF = 1;
26
    public const AAGUID_ASSURANCE_LEVEL_BASIC = 2;
27
    public const AAGUID_ASSURANCE_LEVEL_ATTCA = 3;
28
29
    /**
30
     * the AAGUID of the newly registered authenticator
31
     * @var string
32
     */
33
    protected $AAGUID;
34
35
    /**
36
     * how sure are we about the AAGUID?
37
     * @var int
38
     */
39
    protected $AAGUIDAssurance;
40
41
    /**
42
     * An array of known hardware tokens
43
     *
44
     * @var AAGUID
45
     */
46
    protected $AAGUIDDictionary;
47
48
    /**
49
     * Initialize the event object.
50
     *
51
     * Validates and parses the configuration.
52
     *
53
     * @param string $pubkeyCredType  PublicKeyCredential.type
54
     * @param string $scope           the scope of the event
55
     * @param string $challenge       the challenge which was used to trigger this event
56
     * @param string $idpEntityId     the entity ID of our IdP
57
     * @param string $attestationData the attestation data CBOR blob
58
     * @param string $responseId      the response ID
59
     * @param string $clientDataJSON  the client data JSON string which is present in all types of events
60
     * @param bool $debugMode         print debugging statements?
61
     */
62
    public function __construct(
63
        string $pubkeyCredType,
64
        string $scope,
65
        string $challenge,
66
        string $idpEntityId,
67
        string $attestationData,
68
        string $responseId,
69
        string $clientDataJSON,
70
        bool $debugMode = false
71
    ) {
72
        $this->debugBuffer .= "attestationData raw: " . $attestationData . "<br/>";
73
        /**
74
         * §7.1 STEP 9 : CBOR decode attestationData.
75
         */
76
        $attestationArray = $this->cborDecode($attestationData);
77
        $authData = $attestationArray['authData'];
78
        $this->eventType = "REG";
79
        parent::__construct($pubkeyCredType, $scope, $challenge, $idpEntityId, $authData, $clientDataJSON, $debugMode);
80
81
        $this->AAGUIDDictionary = AAGUID::getInstance();
82
83
        // this function extracts the public key
84
        $this->validateAttestedCredentialData(substr($authData, 37), $responseId);
85
        // this function may need the public key to have been previously extracted
86
        $this->validateAttestationData($attestationData);
87
        // the following function sets the credential properties
88
        $this->debugBuffer .= "Attestation Data (bin2hex): " . bin2hex(substr($authData, 37)) . "<br/>";
89
    }
90
91
    /**
92
     * Validate the incoming attestation data CBOR blob and return the embedded authData
93
     * @param string $attestationData
94
     * @return void
95
     */
96
    private function validateAttestationData(string $attestationData): void
97
    {
98
        /**
99
         * STEP 9 of the validation procedure in § 7.1 of the spec: CBOR-decode the attestationObject
100
         */
101
        $attestationArray = $this->cborDecode($attestationData);
102
        $this->debugBuffer .= "<pre>";
103
        $this->debugBuffer .= print_r($attestationArray, true);
104
        $this->debugBuffer .= "</pre>";
105
106
        /**
107
         * STEP 15 of the validation procedure in § 7.1 of the spec: verify attStmt values
108
         */
109
        switch ($attestationArray['fmt']) {
110
            case "none":
111
                $this->validateAttestationFormatNone($attestationArray);
112
                break;
113
            case "packed":
114
                $this->validateAttestationFormatPacked($attestationArray);
115
                break;
116
            case "fido-u2f":
117
                $this->validateAttestationFormatFidoU2F($attestationArray);
118
                break;
119
            case "android-safetynet":
120
                $this->validateAttestationFormatAndroidSafetyNet($attestationArray);
121
                break;
122
	    case "apple":
123
		$this->validateAttestationFormatApple($attestationArray);
124
		break;
125
            case "tpm":
126
            case "android-key":
127
                $this->fail("Attestation format " . $attestationArray['fmt'] . " validation not supported right now.");
128
                break;
129
            default:
130
                $this->fail("Unknown attestation format.");
131
                break;
132
        }
133
    }
134
135
    /**
136
     * @param array $attestationArray
137
     * @return void
138
     */
139
    private function validateAttestationFormatNone(array $attestationArray): void
140
    {
141
        // § 8.7 of the spec
142
        /**
143
         * § 7.1 Step 16 && §8.7 Verification Procedure: stmt must be an empty array
144
         * § 7.1 Step 17+18 are a NOOP if the format was "none" (which is acceptable as per this RPs policy)
145
         */
146
        if (count($attestationArray['attStmt']) === 0) {
147
            $this->pass("Attestation format and statement as expected, and no attestation authorities to retrieve.");
148
            $this->AAGUIDAssurance = self::AAGUID_ASSURANCE_LEVEL_NONE;
149
            return;
150
        } else {
151
            $this->fail("Non-empty attestation authorities are not expected with 'attestationFormat = none'.");
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]));	
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\webauthn\WebAuthn\Utils\Crypto was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
     * @param array $attestationArray
289
     * @return void
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($this->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($this->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
     * @param array $attestationArray
374
     * @return void
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
     * support legacy U2F tokens
407
     *
408
     * @param array $attestationData the incoming attestation data
409
     * @return void
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 = $this->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
     * support Android authenticators (fingerprint etc.)
480
     *
481
     * @param array $attestationData the incoming attestation data
482
     * @return void
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
     * The registration contains the actual credential. This function parses it.
490
     * @param string $attData    the attestation data binary blob
491
     * @param string $responseId the response ID
492
     * @return void
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
     * transform DER formatted certificate to PEM format
537
     *
538
     * @param string $derData blob of DER data
539
     * @return string the PEM representation of the certificate
540
     */
541
    private function der2pem(string $derData): string
542
    {
543
        $pem = chunk_split(base64_encode($derData), 64, "\n");
544
        $pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
545
        return $pem;
546
    }
547
548
    /**
549
     * @return string
550
     */
551
    public function getAAGUID()
552
    {
553
        return $this->AAGUID;
554
    }
555
}
556