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
|
|
|
|