Passed
Push — master ( 7581d2...f8c35d )
by Thomas
01:38
created

WebAuthnServer::createAuthenticationContext()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4.0092

Importance

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