Completed
Push — master ( 698a0f...8180db )
by Florent
01:57
created

AuthenticatorAssertionResponseValidator::check()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 71
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 30
nc 1
nop 4
dl 0
loc 71
rs 9.44
c 0
b 0
f 0

How to fix   Long Method   

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
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace U2FAuthentication\Fido2;
15
16
use Assert\Assertion;
17
use CBOR\Decoder;
18
use CBOR\StringStream;
19
20
class AuthenticatorAssertionResponseValidator
21
{
22
    private $credentialRepository;
23
    private $decoder;
24
25
    public function __construct(CredentialRepository $credentialRepository, Decoder $decoder)
26
    {
27
        $this->credentialRepository = $credentialRepository;
28
        $this->decoder = $decoder;
29
    }
30
31
    /**
32
     * @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
33
     */
34
    public function check(string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ?string $rpId = null): void
35
    {
36
        /* @see 7.2.1 */
37
        Assertion::true($this->isCredentialIdAllowed($credentialId, $publicKeyCredentialRequestOptions->getAllowCredentials()), 'The credential ID is not allowed.');
38
39
        /* @see 7.2.2 */
40
        Assertion::null($authenticatorAssertionResponse->getUserHandle(), 'User Handle not supported.'); //TODO: implementation shall be done.
41
42
        /* @see 7.2.3 */
43
        Assertion::true($this->credentialRepository->has($credentialId), 'No credential public key available for the given credential ID.');
44
45
        $attestedCredentialData = $this->credentialRepository->get($credentialId);
46
        $credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
47
        Assertion::notNull($credentialPublicKey, 'No public key available.');
48
49
        $credentialPublicKey = $this->decoder->decode(
50
            new StringStream($credentialPublicKey)
51
        );
52
53
        /** @see 7.2.4 */
54
        /** @see 7.2.5 */
55
        //Nothing to do. Use of objets directly
56
57
        /** @see 7.2.6 */
58
        $C = $authenticatorAssertionResponse->getClientDataJSON();
59
60
        /* @see 7.2.7 */
61
        Assertion::eq('webauthn.get', $C->getType(), 'The client data type is not "webauthn.get".');
62
63
        /* @see 7.2.8 */
64
        Assertion::true(hash_equals($publicKeyCredentialRequestOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
65
66
        /** @see 7.2.9 */
67
        $rpId = $rpId ?? $publicKeyCredentialRequestOptions->getRpId();
68
        Assertion::notNull($rpId, 'No rpId.');
69
70
        $parsedRelyingPartyId = parse_url($C->getOrigin());
71
        Assertion::true(array_key_exists('host', $parsedRelyingPartyId) && \is_string($parsedRelyingPartyId['host']), 'Invalid origin rpId.');
72
73
        Assertion::eq($parsedRelyingPartyId['host'], $rpId, 'rpId mismatch.');
74
75
        /* @see 7.2.10 */
76
        Assertion::null($C->getTokenBinding(), 'Token binding not supported.');
77
78
        /** @see 7.2.11 */
79
        $rpIdHash = hash('sha256', $rpId, true);
80
        Assertion::true(hash_equals($rpIdHash, $authenticatorAssertionResponse->getAuthenticatorData()->getRpIdHash()), 'rpId hash mismatch.');
81
82
        /* @see 7.2.12 */
83
        Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserPresent(), 'User was not present');
84
85
        /* @see 7.2.13 */
86
        Assertion::false(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialRequestOptions->getUserVerification() && !$authenticatorAssertionResponse->getAuthenticatorData()->isUserVerified(), 'User authentication required.');
87
88
        /* @see 7.2.14 */
89
        Assertion::eq(0, $publicKeyCredentialRequestOptions->getExtensions()->count(), 'Extensions not supported.');
90
91
        /** @see 7.2.15 */
92
        $getClientDataJSONHash = hash('sha256', $authenticatorAssertionResponse->getClientDataJSON()->getRawData(), true);
93
94
        /* @see 7.2.16 */
95
        $coseKey = $credentialPublicKey->getNormalizedData();
96
        $key = "\04".$coseKey[-2].$coseKey[-3];
97
        Assertion::eq(1, openssl_verify($authenticatorAssertionResponse->getAuthenticatorData()->getAuthData().$getClientDataJSONHash, $authenticatorAssertionResponse->getSignature(), $this->getPublicKeyAsPem($key), OPENSSL_ALGO_SHA256), 'Invalid signature.');
98
99
        /* @see 7.2.17 */
100
        $storedCounter = $this->credentialRepository->getCounterFor($credentialId);
101
        $currentCounter = $authenticatorAssertionResponse->getAuthenticatorData()->getSignCount();
102
        Assertion::greaterThan($currentCounter, $storedCounter, 'Invalid counter.');
103
104
        $this->credentialRepository->updateCounterFor($credentialId, $currentCounter);
105
106
        /* @see 7.2.18 */
107
    }
108
109
    private function getPublicKeyAsPem(string $key): string
110
    {
111
        $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
112
        $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
113
        $der .= "\0".$key;
114
115
        $pem = '-----BEGIN PUBLIC KEY-----'.PHP_EOL;
116
        $pem .= chunk_split(base64_encode($der), 64, PHP_EOL);
117
        $pem .= '-----END PUBLIC KEY-----'.PHP_EOL;
118
119
        return $pem;
120
    }
121
122
    private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool
123
    {
124
        foreach ($allowedCredentials as $allowedCredential) {
125
            if (hash_equals($allowedCredential->getId(), $credentialId)) {
126
                return true;
127
            }
128
        }
129
130
        return false;
131
    }
132
}
133