madwizard-org /
webauthn-server
| 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
Loading history...
|
|||
| 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 |