verifyAuthenticatonAssertion()   B
last analyzed

Complexity

Conditions 11
Paths 9

Size

Total Lines 80
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 11.968

Importance

Changes 0
Metric Value
cc 11
eloc 24
c 0
b 0
f 0
nc 9
nop 2
dl 0
loc 80
ccs 20
cts 25
cp 0.8
crap 11.968
rs 7.3166

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 MadWizard\WebAuthn\Server\Authentication;
4
5
use MadWizard\WebAuthn\Attestation\AuthenticatorData;
6
use MadWizard\WebAuthn\Credential\CredentialId;
7
use MadWizard\WebAuthn\Credential\CredentialStoreInterface;
8
use MadWizard\WebAuthn\Credential\UserCredentialInterface;
9
use MadWizard\WebAuthn\Crypto\CoseKeyInterface;
10
use MadWizard\WebAuthn\Dom\AuthenticatorAssertionResponseInterface;
11
use MadWizard\WebAuthn\Dom\CollectedClientData;
12
use MadWizard\WebAuthn\Dom\PublicKeyCredentialInterface;
13
use MadWizard\WebAuthn\Dom\TokenBindingStatus;
14
use MadWizard\WebAuthn\Exception\UnsupportedException;
15
use MadWizard\WebAuthn\Exception\VerificationException;
16
use MadWizard\WebAuthn\Extension\ExtensionInterface;
17
use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface;
18
use MadWizard\WebAuthn\Format\ByteBuffer;
19
use MadWizard\WebAuthn\Server\AbstractVerifier;
20
21
final class AuthenticationVerifier extends AbstractVerifier
22
{
23
    /**
24
     * @var CredentialStoreInterface
25
     */
26
    private $credentialCollection;
27
28 11
    public function __construct(CredentialStoreInterface $credentialCollection, ExtensionRegistryInterface $extensionRegistry)
29
    {
30 11
        parent::__construct($extensionRegistry);
31 11
        $this->credentialCollection = $credentialCollection;
32 11
    }
33
34 11
    public function verifyAuthenticatonAssertion(PublicKeyCredentialInterface $credential, AuthenticationContext $context): AuthenticationResult
35
    {
36
        // SPEC 7.2 Verifying an authentication assertion
37
38 11
        $response = $credential->getResponse()->asAssertionResponse();
39 11
        $authData = new AuthenticatorData($response->getAuthenticatorData());
40 11
        $extensionContext = $this->processExtensions($credential, $authData, $context, ExtensionInterface::OPERATION_AUTHENTICATION);
41
42
        // 1. If the allowCredentials option was given when this authentication ceremony was initiated, verify that
43
        //    credential.id identifies one of the public key credentials that were listed in allowCredentials.
44 11
        if (!$this->checkAllowCredentials($credential, $context->getAllowCredentialIds())) {
45 1
            throw new VerificationException('Credential not in list of allowed credentials.');
46
        }
47
48
        // Note: step 2 done after 3 because credential is available then.
49
        // 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is inappropriate for
50
        // your use case), look up the corresponding credential public key.
51 10
        $accountCredential = $this->credentialCollection->findCredential(CredentialId::fromBinary($credential->getRawId()->getBinaryString()));
52 10
        if ($accountCredential === null) {
53
            throw new VerificationException('Account was not found');
54
        }
55
56
        // 2. If credential.response.userHandle is present, verify that the user identified by this value is the owner
57
        //    of the public key credential identified by credential.id.
58 10
        if ($response->getUserHandle() !== null && !$response->getUserHandle()->equals($accountCredential->getUserHandle()->toBuffer())) {
59 1
            throw new VerificationException("Credential does not belong to the user identified by the client's userHandle.");
60
        }
61
62
        // 4. Let cData, aData and sig denote the value of credential’s response's clientDataJSON, authenticatorData,
63
        //    and signature respectively.
64
        // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
65
        // 6. Let C, the client data claimed as used for the signature, be the result of running an
66
        //    implementation-specific JSON parser on JSONtext.
67
        // 7 - 10
68 9
        $clientData = $response->getParsedClientData();
69 9
        $this->checkClientData($clientData, $context);
70
71
        // 11. Verify that the rpIdHash in aData is the SHA-256 hash of the RP ID expected by the Relying Party.
72 5
        if (!$this->verifyRpIdHash($authData, $context, $extensionContext)) {
73 1
            throw new VerificationException('rpIdHash was not correct.');
74
        }
75
76
        // 12 and 13
77 4
        if (!$this->verifyUser($authData, $context)) {
78
            throw new VerificationException('User verification failed');
79
        }
80
81
        // 14. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator
82
        //     extension outputs in the extensions in authData are as expected, considering the client extension input
83
        //     values that were given as the extensions option in the get() call. In particular, any extension
84
        //     identifier values in the clientExtensionResults and the extensions in authData MUST be also be present
85
        //     as extension identifier values in the extensions member of options, i.e., no extensions are present that
86
        //     were not requested. In the general case, the meaning of "are as expected" is specific to the Relying
87
        //     Party and which extensions are in use.
88
        //     Note: Since all extensions are OPTIONAL for both the client and the authenticator, the Relying Party MUST
89
        //     be prepared to handle cases where none or not all of the requested extensions were acted upon.
90
91
        // -> This is already checked in processExtensions above, the extensions need to be processed earlier because
92
        // extensions such as appid affect the effective rpId
93
94
        // 15 and 16
95 4
        if (!$this->verifySignature($response, $accountCredential->getPublicKey())) {
96
            throw new VerificationException('Invalid signature');
97
        }
98
99
        // 17 and 18
100 4
        if (!$this->verifySignatureCounter($authData, $accountCredential)) {
101
            throw new VerificationException('Signature counter invalid');
102
        }
103
104
        // If all the above steps are successful, continue with the authentication ceremony as appropriate. Otherwise, fail the authentication ceremony.
105
106
        // Additional custom checks:
107
        // Ensure credential belongs to user being authenticated
108 4
        $userHandle = $context->getUserHandle();
109 4
        if ($userHandle !== null && !$userHandle->equals($accountCredential->getUserHandle())) {
110
            throw new VerificationException('Credential does not belong to the user currently being authenticated.');
111
        }
112
113 4
        return new AuthenticationResult($accountCredential, $authData);
114
    }
115
116
    /**
117
     * @param CredentialId[]|null $allowCredentialIds
118
     */
119 11
    private function checkAllowCredentials(PublicKeyCredentialInterface $credential, ?array $allowCredentialIds): bool
120
    {
121 11
        if ($allowCredentialIds === null || \count($allowCredentialIds) === 0) {
122
            return true;
123
        }
124
125 11
        $credentialId = CredentialId::fromBuffer($credential->getRawId());
126 11
        foreach ($allowCredentialIds as $allowCredentialId) {
127 11
            if ($allowCredentialId->equals($credentialId)) {
128 10
                return true;
129
            }
130
        }
131 1
        return false;
132
    }
133
134 4
    private function verifySignature(AuthenticatorAssertionResponseInterface $response, CoseKeyInterface $publicKey): bool
135
    {
136
        // 15. Let hash be the result of computing a hash over the cData using SHA-256.
137 4
        $clientData = $response->getClientDataJson();
138 4
        $clientDataHash = hash('sha256', $clientData, true);
139
140
        // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature over the binary concatenation of aData and hash.
141 4
        $aData = $response->getAuthenticatorData()->getBinaryString();
142
143 4
        $signData = $aData . $clientDataHash;
144
145 4
        return $publicKey->verifySignature(new ByteBuffer($signData), $response->getSignature());
146
    }
147
148 4
    private function verifySignatureCounter(AuthenticatorData $authData, UserCredentialInterface $accountCredential): bool
149
    {
150
        // 17. If the signature counter value adata.signCount is nonzero or the value stored in conjunction with credential’s id attribute is nonzero, then run the following sub-step:
151 4
        $counter = $authData->getSignCount();
152 4
        if ($counter === 0) {
153
            return true;
154
        }
155
156 4
        $lastCounter = $this->credentialCollection->getSignatureCounter($accountCredential->getCredentialId());
157
158 4
        if ($lastCounter === null) {
159
            // counter not known
160
            $this->credentialCollection->updateSignatureCounter($accountCredential->getCredentialId(), $counter);
161
162
            // TODO policy
163
            return true;
164
        }
165
166 4
        if ($lastCounter === 0) {
167
            return true;
168
        }
169 4
        if ($counter > $lastCounter) {
170
            // 18. If the signature counter value adata.signCount is
171
            // -> greater than the signature counter value stored in conjunction with credential’s id attribute.
172
            //    Update the stored signature counter value, associated with credential’s id attribute, to be the value of adata.signCount.
173 4
            $this->credentialCollection->updateSignatureCounter($accountCredential->getCredentialId(), $counter);
174 4
            return true;
175
        } else {
176
            // -> less than or equal to the signature counter value stored in conjunction with credential’s id attribute.
177
            //    This is a signal that the authenticator may be cloned, i.e. at least two copies of the credential private key may exist and are being used in
178
            // parallel. Relying Parties should incorporate this information into their risk scoring. Whether the Relying Party updates the stored signature counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.
179
180
            // TODO add policy
181
            return false;
182
        }
183
    }
184
185 9
    private function checkClientData(CollectedClientData $clientData, AuthenticationContext $context): void
186
    {
187
        // 7. Verify that the value of C.type is the string webauthn.get.
188 9
        if ($clientData->getType() !== 'webauthn.get') {
189 1
            throw new VerificationException('Expecting type in clientDataJSON to be webauthn.get.');
190
        }
191
192
        // 8. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the
193
        //    PublicKeyCredentialRequestOptions passed to the get() call.
194 8
        if (!\hash_equals($context->getChallenge()->getBase64Url(), $clientData->getChallenge())) {
195 1
            throw new VerificationException('Challenge in clientDataJSON does not match the challenge in the request.');
196
        }
197
198
        // 9. Verify that the value of C.origin matches the Relying Party's origin.
199 7
        if (!$this->verifyOrigin($clientData->getOrigin(), $context->getOrigin())) {
200 1
            throw new VerificationException(sprintf("Origin '%s' does not match relying party origin.", $clientData->getOrigin()));
201
        }
202
203
        // 10. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection
204
        //     over which the attestation was obtained. If Token Binding was used on that TLS connection, also verify
205
        //     that C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
206 6
        $tokenBinding = $clientData->getTokenBinding();
207 6
        if ($tokenBinding !== null && $tokenBinding->getStatus() === TokenBindingStatus::PRESENT) {
208 1
            throw new UnsupportedException('Token binding is not yet supported by this library.');
209
        }
210 5
    }
211
}
212