Passed
Push — master ( 022bc2...5c7e77 )
by Thomas
03:17
created

WebAuthnServer::createAuthenticatorSelection()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.024

Importance

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