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