1 | <?php |
||
2 | |||
3 | namespace MadWizard\WebAuthn\Server; |
||
4 | |||
5 | use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistryInterface; |
||
6 | use MadWizard\WebAuthn\Config\RelyingPartyInterface; |
||
7 | use MadWizard\WebAuthn\Credential\CredentialId; |
||
8 | use MadWizard\WebAuthn\Credential\CredentialStoreInterface; |
||
9 | use MadWizard\WebAuthn\Credential\UserHandle; |
||
10 | use MadWizard\WebAuthn\Dom\AuthenticationExtensionsClientInputs; |
||
11 | use MadWizard\WebAuthn\Dom\AuthenticatorSelectionCriteria; |
||
12 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialCreationOptions; |
||
13 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialDescriptor; |
||
14 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialInterface; |
||
15 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialParameters; |
||
16 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialRequestOptions; |
||
17 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialRpEntity; |
||
18 | use MadWizard\WebAuthn\Dom\PublicKeyCredentialUserEntity; |
||
19 | use MadWizard\WebAuthn\Dom\ResidentKeyRequirement; |
||
20 | use MadWizard\WebAuthn\Dom\UserVerificationRequirement; |
||
21 | use MadWizard\WebAuthn\Exception\CredentialIdExistsException; |
||
22 | use MadWizard\WebAuthn\Exception\NoCredentialsException; |
||
23 | use MadWizard\WebAuthn\Exception\UntrustedException; |
||
24 | use MadWizard\WebAuthn\Exception\VerificationException; |
||
25 | use MadWizard\WebAuthn\Exception\WebAuthnException; |
||
26 | use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface; |
||
27 | use MadWizard\WebAuthn\Format\ByteBuffer; |
||
28 | use MadWizard\WebAuthn\Metadata\MetadataResolverInterface; |
||
29 | use MadWizard\WebAuthn\Policy\PolicyInterface; |
||
30 | use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManagerInterface; |
||
31 | use MadWizard\WebAuthn\Server\Authentication\AuthenticationContext; |
||
32 | use MadWizard\WebAuthn\Server\Authentication\AuthenticationOptions; |
||
33 | use MadWizard\WebAuthn\Server\Authentication\AuthenticationRequest; |
||
34 | use MadWizard\WebAuthn\Server\Authentication\AuthenticationResultInterface; |
||
35 | use MadWizard\WebAuthn\Server\Authentication\AuthenticationVerifier; |
||
36 | use MadWizard\WebAuthn\Server\Registration\RegistrationContext; |
||
37 | use MadWizard\WebAuthn\Server\Registration\RegistrationOptions; |
||
38 | use MadWizard\WebAuthn\Server\Registration\RegistrationRequest; |
||
39 | use MadWizard\WebAuthn\Server\Registration\RegistrationResultInterface; |
||
40 | use MadWizard\WebAuthn\Server\Registration\RegistrationVerifier; |
||
41 | use Psr\Log\LoggerAwareInterface; |
||
42 | use Psr\Log\LoggerAwareTrait; |
||
43 | use Psr\Log\NullLogger; |
||
44 | |||
45 | final class WebAuthnServer implements ServerInterface, LoggerAwareInterface |
||
46 | { |
||
47 | use LoggerAwareTrait; |
||
48 | |||
49 | /** |
||
50 | * @var RelyingPartyInterface |
||
51 | */ |
||
52 | private $relyingParty; |
||
53 | |||
54 | /** |
||
55 | * @var PolicyInterface |
||
56 | */ |
||
57 | private $policy; |
||
58 | |||
59 | /** |
||
60 | * @var CredentialStoreInterface |
||
61 | */ |
||
62 | private $credentialStore; |
||
63 | |||
64 | /** |
||
65 | * @var AttestationFormatRegistryInterface |
||
66 | */ |
||
67 | private $formatRegistry; |
||
68 | |||
69 | /** |
||
70 | * @var MetadataResolverInterface |
||
71 | */ |
||
72 | private $metadataResolver; |
||
73 | |||
74 | /** |
||
75 | * @var TrustDecisionManagerInterface |
||
76 | */ |
||
77 | private $trustDecisionManager; |
||
78 | |||
79 | /** |
||
80 | * @var ExtensionRegistryInterface |
||
81 | */ |
||
82 | private $extensionRegistry; |
||
83 | |||
84 | 18 | public function __construct( |
|
85 | RelyingPartyInterface $relyingParty, |
||
86 | PolicyInterface $policy, |
||
87 | CredentialStoreInterface $credentialStore, |
||
88 | AttestationFormatRegistryInterface $formatRegistry, |
||
89 | MetadataResolverInterface $metadataResolver, |
||
90 | TrustDecisionManagerInterface $trustDecisionManager, |
||
91 | ExtensionRegistryInterface $extensionRegistry |
||
92 | ) { |
||
93 | 18 | $this->relyingParty = $relyingParty; |
|
94 | 18 | $this->policy = $policy; |
|
95 | 18 | $this->credentialStore = $credentialStore; |
|
96 | 18 | $this->formatRegistry = $formatRegistry; |
|
97 | 18 | $this->metadataResolver = $metadataResolver; |
|
98 | 18 | $this->trustDecisionManager = $trustDecisionManager; |
|
99 | 18 | $this->extensionRegistry = $extensionRegistry; |
|
100 | 18 | $this->logger = new NullLogger(); |
|
101 | 18 | } |
|
102 | |||
103 | 1 | public function startRegistration(RegistrationOptions $options): RegistrationRequest |
|
104 | { |
||
105 | 1 | $challenge = $this->createChallenge(); |
|
106 | |||
107 | 1 | $creationOptions = new PublicKeyCredentialCreationOptions( |
|
108 | 1 | PublicKeyCredentialRpEntity::fromRelyingParty($this->relyingParty), |
|
109 | 1 | $this->createUserEntity($options->getUser()), |
|
110 | $challenge, |
||
111 | 1 | $this->getCredentialParameters() |
|
112 | ); |
||
113 | |||
114 | 1 | $creationOptions->setAttestation($options->getAttestation()); |
|
115 | 1 | $creationOptions->setTimeout($options->getTimeout()); |
|
116 | |||
117 | 1 | $selection = $this->createAuthenticatorSelection($options); |
|
118 | |||
119 | 1 | $creationOptions->setAuthenticatorSelection($selection); |
|
120 | 1 | $extensions = $options->getExtensionInputs(); |
|
121 | 1 | if (count($extensions) > 0) { |
|
122 | $creationOptions->setExtensions( |
||
123 | AuthenticationExtensionsClientInputs::fromArray($extensions) |
||
124 | ); |
||
125 | } |
||
126 | |||
127 | 1 | if ($options->getExcludeExistingCredentials()) { |
|
128 | $credentialIds = $this->credentialStore->getUserCredentialIds($options->getUser()->getUserHandle()); |
||
129 | foreach ($credentialIds as $credential) { |
||
130 | $creationOptions->addExcludeCredential( |
||
131 | new PublicKeyCredentialDescriptor($credential->toBuffer()) |
||
132 | ); |
||
133 | } |
||
134 | } |
||
135 | |||
136 | 1 | $context = $this->createRegistrationContext($options, $creationOptions); |
|
137 | 1 | return new RegistrationRequest($creationOptions, $context); |
|
138 | } |
||
139 | |||
140 | 1 | private function createAuthenticatorSelection(RegistrationOptions $options): ?AuthenticatorSelectionCriteria |
|
141 | { |
||
142 | 1 | $criteria = null; |
|
143 | 1 | $attachment = $options->getAuthenticatorAttachment(); |
|
144 | 1 | if ($attachment !== null) { |
|
145 | $criteria = new AuthenticatorSelectionCriteria(); |
||
146 | $criteria->setAuthenticatorAttachment($attachment); |
||
147 | } |
||
148 | |||
149 | 1 | $userVerification = $options->getUserVerification(); |
|
150 | 1 | if ($userVerification !== null) { |
|
151 | $criteria = $criteria ?? new AuthenticatorSelectionCriteria(); |
||
152 | $criteria->setUserVerification($userVerification); |
||
153 | } |
||
154 | |||
155 | 1 | $residentKey = $options->getResidentKey(); |
|
156 | 1 | if ($residentKey !== null) { |
|
157 | $criteria = $criteria ?? new AuthenticatorSelectionCriteria(); |
||
158 | $criteria->setRequireResidentKey($residentKey === ResidentKeyRequirement::REQUIRED); |
||
159 | } |
||
160 | 1 | return $criteria; |
|
161 | } |
||
162 | |||
163 | 1 | private function createRegistrationContext(RegistrationOptions $regOptions, PublicKeyCredentialCreationOptions $options): RegistrationContext |
|
164 | { |
||
165 | 1 | $origin = $this->relyingParty->getOrigin(); |
|
166 | 1 | $rpId = $this->relyingParty->getEffectiveId(); |
|
167 | |||
168 | // TODO: mismatch $rp and rp in $options? Check? |
||
169 | 1 | $context = new RegistrationContext($options->getChallenge(), $origin, $rpId, UserHandle::fromBuffer($options->getUserEntity()->getId())); |
|
170 | |||
171 | 1 | $context->setUserPresenceRequired($this->policy->isUserPresenceRequired()); |
|
172 | |||
173 | 1 | if ($regOptions->getUserVerification() === UserVerificationRequirement::REQUIRED) { |
|
174 | $context->setUserVerificationRequired(true); |
||
175 | } |
||
176 | |||
177 | 1 | foreach ($regOptions->getExtensionInputs() as $input) { |
|
178 | $context->addExtensionInput($input); |
||
179 | } |
||
180 | 1 | return $context; |
|
181 | } |
||
182 | |||
183 | /** |
||
184 | * @param PublicKeyCredentialInterface $credential Attestation credential response from the client |
||
185 | * |
||
186 | * @throws CredentialIdExistsException |
||
187 | * @throws VerificationException |
||
188 | */ |
||
189 | 1 | public function finishRegistration(PublicKeyCredentialInterface $credential, RegistrationContext $context): RegistrationResultInterface |
|
190 | { |
||
191 | 1 | $verifier = new RegistrationVerifier($this->formatRegistry, $this->extensionRegistry); |
|
192 | 1 | $registrationResult = $verifier->verify($credential, $context); |
|
193 | |||
194 | 1 | $response = $credential->getResponse()->asAttestationResponse(); |
|
0 ignored issues
–
show
Unused Code
introduced
by
![]() |
|||
195 | |||
196 | // 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root certificates or |
||
197 | // ECDAA-Issuer public keys) for that attestation type and attestation statement format fmt, from a trusted |
||
198 | // source or from policy. |
||
199 | |||
200 | 1 | $metadata = $this->metadataResolver->getMetadata($registrationResult); |
|
201 | 1 | $registrationResult = $registrationResult->withMetadata($metadata); |
|
202 | |||
203 | // 16. Assess the attestation trustworthiness using the outputs of the verification procedure in step 14, |
||
204 | // as follows: |
||
205 | // If self attestation was used, check if self attestation is acceptable under Relying Party policy. |
||
206 | // If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in the |
||
207 | // set of acceptable trust anchors obtained in step 15. |
||
208 | // Otherwise, use the X.509 certificates returned by the verification procedure to verify that the |
||
209 | // attestation public key correctly chains up to an acceptable root certificate. |
||
210 | |||
211 | try { |
||
212 | 1 | $this->trustDecisionManager->verifyTrust($registrationResult, $metadata); |
|
213 | } catch (UntrustedException $e) { |
||
214 | throw new VerificationException('The attestation is not trusted: ' . $e->getReason(), 0, $e); |
||
215 | } |
||
216 | |||
217 | // 17. Check that the credentialId is not yet registered to any other user. If registration is requested for a |
||
218 | // credential that is already registered to a different user, the Relying Party SHOULD fail this |
||
219 | // registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older |
||
220 | // registration. |
||
221 | 1 | if ($this->credentialStore->findCredential($registrationResult->getCredentialId())) { |
|
222 | throw new CredentialIdExistsException('Credential is already registered.'); |
||
223 | } |
||
224 | |||
225 | // 18. If the attestation statement attStmt verified successfully and is found to be trustworthy, then register |
||
226 | // the new credential with the account that was denoted in the options.user passed to create(), by |
||
227 | // associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, |
||
228 | // as appropriate for the Relying Party's system. |
||
229 | // 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above, |
||
230 | // the Relying Party SHOULD fail the registration ceremony. |
||
231 | // |
||
232 | // NOTE: However, if permitted by policy, the Relying Party MAY register the credential ID and credential |
||
233 | // public key but treat the credential as one with self attestation (see §6.3.3 Attestation Types). |
||
234 | // If doing so, the Relying Party is asserting there is no cryptographic proof that the public key credential |
||
235 | // has been generated by a particular authenticator model. See [FIDOSecRef] and [UAFProtocol] for a more |
||
236 | // detailed discussion. |
||
237 | // |
||
238 | // Verification of attestation objects requires that the Relying Party has a trusted method of determining |
||
239 | // acceptable trust anchors in step 15 above. Also, if certificates are being used, the Relying Party MUST |
||
240 | // have access to certificate status information for the intermediate CA certificates. The Relying Party MUST |
||
241 | // also be able to build the attestation certificate chain if the client did not provide this chain in the |
||
242 | // attestation information. |
||
243 | 1 | return $registrationResult; |
|
244 | } |
||
245 | |||
246 | 1 | public function startAuthentication(AuthenticationOptions $options): AuthenticationRequest |
|
247 | { |
||
248 | 1 | $challenge = $this->createChallenge(); |
|
249 | |||
250 | 1 | $requestOptions = new PublicKeyCredentialRequestOptions($challenge); |
|
251 | 1 | $requestOptions->setRpId($this->relyingParty->getId()); |
|
252 | 1 | $uv = $options->getUserVerification(); |
|
253 | 1 | if ($uv !== UserVerificationRequirement::DEFAULT) { |
|
254 | 1 | $requestOptions->setUserVerification($uv); |
|
255 | } |
||
256 | 1 | $requestOptions->setTimeout($options->getTimeout()); |
|
257 | |||
258 | 1 | $this->addAllowCredentials($options, $requestOptions); |
|
259 | |||
260 | 1 | $extensions = $options->getExtensionInputs(); |
|
261 | 1 | if (count($extensions) > 0) { |
|
262 | $requestOptions->setExtensions( |
||
263 | AuthenticationExtensionsClientInputs::fromArray($extensions) |
||
264 | ); |
||
265 | } |
||
266 | |||
267 | 1 | $context = $this->createAuthenticationContext($options, $requestOptions); |
|
268 | 1 | return new AuthenticationRequest($requestOptions, $context); |
|
269 | } |
||
270 | |||
271 | 1 | private function createAuthenticationContext(AuthenticationOptions $authOptions, PublicKeyCredentialRequestOptions $options): AuthenticationContext |
|
272 | { |
||
273 | 1 | $origin = $this->relyingParty->getOrigin(); |
|
274 | 1 | $rpId = $this->relyingParty->getEffectiveId(); |
|
275 | |||
276 | // TODO: mismatch $rp and rp in $policy? Check? |
||
277 | 1 | $context = new AuthenticationContext($options->getChallenge(), $origin, $rpId, $authOptions->getUserHandle()); |
|
278 | |||
279 | 1 | if ($authOptions->getUserVerification() === UserVerificationRequirement::REQUIRED) { |
|
280 | $context->setUserVerificationRequired(true); |
||
281 | } |
||
282 | |||
283 | 1 | $context->setUserPresenceRequired($this->policy->isUserPresenceRequired()); |
|
284 | |||
285 | 1 | $allowCredentials = $options->getAllowCredentials(); |
|
286 | 1 | if ($allowCredentials !== null) { |
|
287 | 1 | foreach ($allowCredentials as $credential) { |
|
288 | 1 | $context->addAllowCredentialId(CredentialId::fromBuffer($credential->getId())); |
|
289 | } |
||
290 | } |
||
291 | 1 | foreach ($authOptions->getExtensionInputs() as $input) { |
|
292 | $context->addExtensionInput($input); |
||
293 | } |
||
294 | 1 | return $context; |
|
295 | } |
||
296 | |||
297 | /** |
||
298 | * @param PublicKeyCredentialInterface $credential Assertion credential response from the client |
||
299 | * |
||
300 | * @throws VerificationException |
||
301 | */ |
||
302 | 11 | public function finishAuthentication(PublicKeyCredentialInterface $credential, AuthenticationContext $context): AuthenticationResultInterface |
|
303 | { |
||
304 | 11 | $verifier = new AuthenticationVerifier($this->credentialStore, $this->extensionRegistry); |
|
305 | |||
306 | 11 | $authenticationResult = $verifier->verifyAuthenticatonAssertion($credential, $context); |
|
307 | |||
308 | 4 | return $authenticationResult; |
|
309 | } |
||
310 | |||
311 | /** |
||
312 | * @throws WebAuthnException |
||
313 | */ |
||
314 | 1 | private function addAllowCredentials(AuthenticationOptions $options, PublicKeyCredentialRequestOptions $requestOptions): void |
|
315 | { |
||
316 | 1 | $userHandle = $options->getUserHandle(); |
|
317 | 1 | if ($userHandle !== null) { |
|
318 | $credentialIds = $this->credentialStore->getUserCredentialIds($userHandle); |
||
319 | if (count($credentialIds) === 0) { |
||
320 | throw new NoCredentialsException('User being authenticated has no credentials.'); |
||
321 | } |
||
322 | foreach ($credentialIds as $credentialId) { |
||
323 | $descriptor = new PublicKeyCredentialDescriptor($credentialId->toBuffer()); |
||
324 | $requestOptions->addAllowedCredential($descriptor); |
||
325 | } |
||
326 | } |
||
327 | |||
328 | 1 | $credentialIds = $options->getAllowCredentials(); |
|
329 | 1 | if (count($credentialIds) > 0) { |
|
330 | 1 | foreach ($credentialIds as $credential) { |
|
331 | 1 | $credentialId = $credential->toBuffer(); |
|
332 | 1 | $descriptor = new PublicKeyCredentialDescriptor($credentialId); |
|
333 | // TODO |
||
334 | // foreach ($transports as $transport) { |
||
335 | // $descriptor->addTransport($transport); |
||
336 | // } |
||
337 | 1 | $requestOptions->addAllowedCredential($descriptor); |
|
338 | } |
||
339 | } |
||
340 | 1 | } |
|
341 | |||
342 | 1 | private function createUserEntity(UserIdentityInterface $user): PublicKeyCredentialUserEntity |
|
343 | { |
||
344 | 1 | return new PublicKeyCredentialUserEntity( |
|
345 | 1 | $user->getUsername(), |
|
346 | 1 | $user->getUserHandle()->toBuffer(), |
|
347 | 1 | $user->getDisplayName() |
|
348 | ); |
||
349 | } |
||
350 | |||
351 | /** |
||
352 | * @return PublicKeyCredentialParameters[] |
||
353 | */ |
||
354 | 1 | private function getCredentialParameters(): array |
|
355 | { |
||
356 | 1 | $parameters = []; |
|
357 | 1 | $algorithms = $this->policy->getAllowedAlgorithms(); // TODO: verify server side? |
|
358 | 1 | foreach ($algorithms as $algorithm) { |
|
359 | 1 | $parameters[] = new PublicKeyCredentialParameters($algorithm); |
|
360 | } |
||
361 | 1 | return $parameters; |
|
362 | } |
||
363 | |||
364 | 2 | private function createChallenge(): ByteBuffer |
|
365 | { |
||
366 | 2 | return ByteBuffer::randomBuffer($this->policy->getChallengeLength()); |
|
367 | } |
||
368 | } |
||
369 |