Passed
Push — master ( 5271f3...ae4caa )
by Thomas
02:30
created

WebAuthnServer::createAuthenticationContext()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0729

Importance

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