Passed
Push — master ( d96c64...807a21 )
by Florent
01:39
created

AuthenticatorAssertionResponseValidator::check()   D

Complexity

Conditions 18
Paths 16

Size

Total Lines 90
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 41
nc 16
nop 4
dl 0
loc 90
rs 4.8666
c 0
b 0
f 0

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