Passed
Push — master ( 4c571a...c0376f )
by Thomas
02:30
created

WebAuthnServer::createRegistrationContext()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.072

Importance

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