RegistrationVerifier::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
namespace MadWizard\WebAuthn\Server\Registration;
4
5
use MadWizard\WebAuthn\Attestation\AttestationObject;
6
use MadWizard\WebAuthn\Attestation\AuthenticatorData;
7
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistryInterface;
8
use MadWizard\WebAuthn\Credential\CredentialId;
9
use MadWizard\WebAuthn\Dom\CollectedClientData;
10
use MadWizard\WebAuthn\Dom\PublicKeyCredentialInterface;
11
use MadWizard\WebAuthn\Dom\TokenBindingStatus;
12
use MadWizard\WebAuthn\Exception\FormatNotSupportedException;
13
use MadWizard\WebAuthn\Exception\UnsupportedException;
14
use MadWizard\WebAuthn\Exception\VerificationException;
15
use MadWizard\WebAuthn\Extension\ExtensionInterface;
16
use MadWizard\WebAuthn\Extension\ExtensionProcessingContext;
17
use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface;
18
use MadWizard\WebAuthn\Server\AbstractVerifier;
19
20
final class RegistrationVerifier extends AbstractVerifier
21
{
22
    /**
23
     * @var AttestationFormatRegistryInterface
24
     */
25
    private $formatRegistry;
26
27 1
    public function __construct(AttestationFormatRegistryInterface $registry, ExtensionRegistryInterface $extensionRegistry)
28
    {
29 1
        parent::__construct($extensionRegistry);
30 1
        $this->formatRegistry = $registry;
31 1
    }
32
33
    /**
34
     * @throws VerificationException
35
     * @throws \MadWizard\WebAuthn\Exception\ParseException
36
     * @throws \MadWizard\WebAuthn\Exception\WebAuthnException
37
     */
38 1
    public function verify(PublicKeyCredentialInterface $credential, RegistrationContext $context): RegistrationResult
39
    {
40
        // SPEC 7.1 Registering a new credential
41 1
        $response = $credential->getResponse()->asAttestationResponse();
42
43
        // 1. Let JSONtext be the result of running UTF-8 decode on the value of response.clientDataJSON.
44
        // 2. Let C, the client data claimed as collected during the credential creation, be the result of running an
45
        //    implementation-specific JSON parser on JSONtext.
46
        // 3 - 6
47 1
        $clientData = $response->getParsedClientData();
48 1
        $this->checkClientData($clientData, $context);
49
50
        // 7. Compute the hash of response.clientDataJSON using SHA-256.
51 1
        $clientDataHash = $this->getClientDataHash($response);
52
53
        // 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse structure to
54
        //    obtain the attestation statement format fmt, the authenticator data authData, and the attestation
55
        //    statement attStmt.
56
57 1
        $attestation = AttestationObject::parse($response->getAttestationObject());
58 1
        $authData = new AuthenticatorData($attestation->getAuthenticatorData());
59
60 1
        $extensionContext = $this->processExtensions($credential, $authData, $context, ExtensionInterface::OPERATION_REGISTRATION);
61
62
        // 9 - 11
63 1
        $this->checkAuthenticatorData($authData, $context, $extensionContext);
64
65
        // 12. Verify that the values of the client extension outputs in clientExtensionResults and the authenticator
66
        //     extension outputs in the extensions in authData are as expected, considering the client extension input
67
        //     values that were given as the extensions option in the create() call. In particular, any extension
68
        //     identifier values in the clientExtensionResults and the extensions in authData MUST be also be present
69
        //     as extension identifier values in the extensions member of options, i.e., no extensions are present that
70
        //     were not requested. In the general case, the meaning of "are as expected" is specific to the
71
        //     Relying Party and which extensions are in use.
72
        //     Note: Since all extensions are OPTIONAL for both the client and the authenticator, the Relying Party MUST
73
        //     be prepared to handle cases where none or not all of the requested extensions were acted upon.
74
75
        // -> This is already checked in processExtensions above, the extensions need to be processed earlier because
76
        // extensions such as appid affect the effective rpId
77
78
        // 13. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against
79
        //     the set of supported WebAuthn Attestation Statement Format Identifier values.
80 1
        $format = $attestation->getFormat();
81
82
        try {
83 1
            $statement = $this->formatRegistry->createStatement($attestation);
84 1
            $verifier = $this->formatRegistry->getVerifier($attestation->getFormat());
85
        } catch (FormatNotSupportedException $e) {
86
            throw new VerificationException(sprintf("Attestation format '%s' not supported", $format), 0, $e);
87
        }
88
89
        // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by
90
        //     using the attestation statement format fmt’s verification procedure given attStmt, authData and the hash
91
        //     of the serialized client data computed in step 7.
92 1
        $verificationResult = $verifier->verify($statement, $authData, $clientDataHash);
93
94 1
        return new RegistrationResult(CredentialId::fromBuffer($credential->getRawId()), $authData, $attestation, $verificationResult, $context->getUserHandle());
95
    }
96
97 1
    private function checkClientData(CollectedClientData $clientData, RegistrationContext $context): void
98
    {
99
        // 3. Verify that the value of C.type is webauthn.create.
100 1
        if ($clientData->getType() !== 'webauthn.create') {
101
            throw new VerificationException('Expecting type in clientDataJSON to be webauthn.create.');
102
        }
103
104
        // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator
105
        //    in the create() call.
106 1
        if (!hash_equals($context->getChallenge()->getBase64Url(), $clientData->getChallenge())) {
107
            throw new VerificationException('Challenge in clientDataJSON does not match the challenge in the request.');
108
        }
109
110
        // 5. Verify that the value of C.origin matches the Relying Party's origin.
111 1
        if (!$this->verifyOrigin($clientData->getOrigin(), $context->getOrigin())) {
112
            throw new VerificationException(sprintf("Origin '%s' does not match relying party origin.", $clientData->getOrigin()));
113
        }
114
115
        // 6. Verify that the value of C.tokenBinding.status matches the state of Token Binding for the TLS connection
116
        //    over which the assertion was obtained. If Token Binding was used on that TLS connection, also verify that
117
        //    C.tokenBinding.id matches the base64url encoding of the Token Binding ID for the connection.
118 1
        $tokenBinding = $clientData->getTokenBinding();
119 1
        if ($tokenBinding !== null && $tokenBinding->getStatus() === TokenBindingStatus::PRESENT) {
120
            throw new UnsupportedException('Token binding is not yet supported by this library.');
121
        }
122 1
    }
123
124 1
    private function checkAuthenticatorData(AuthenticatorData $authData, RegistrationContext $context, ExtensionProcessingContext $extensionContext): void
125
    {
126 1
        if (!$authData->hasAttestedCredentialData()) {
127
            throw new VerificationException('Authenticator data does not contain attested credential.');
128
        }
129
130
        // 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
131 1
        if (!$this->verifyRpIdHash($authData, $context, $extensionContext)) {
132
            throw new VerificationException('RP ID hash in authData does not match.');
133
        }
134
135
        // 10 and 11
136 1
        if (!$this->verifyUser($authData, $context)) {
137
            throw new VerificationException('User verification failed');
138
        }
139 1
    }
140
}
141