Passed
Push — master ( 83aa3d...907404 )
by Thomas
07:17
created

ServerBuilder::createMetadataProviders()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 1
b 0
f 0
nc 13
nop 1
dl 0
loc 26
ccs 0
cts 17
cp 0
crap 56
rs 8.8333
1
<?php
2
3
namespace MadWizard\WebAuthn\Builder;
4
5
use Closure;
6
use GuzzleHttp\Client;
7
use MadWizard\WebAuthn\Attestation\AttestationType;
8
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistry;
9
use MadWizard\WebAuthn\Attestation\Registry\AttestationFormatRegistryInterface;
10
use MadWizard\WebAuthn\Attestation\TrustAnchor\TrustPathValidator;
11
use MadWizard\WebAuthn\Attestation\TrustAnchor\TrustPathValidatorInterface;
12
use MadWizard\WebAuthn\Attestation\Verifier;
13
use MadWizard\WebAuthn\Cache\CacheProviderInterface;
14
use MadWizard\WebAuthn\Cache\FileCacheProvider;
15
use MadWizard\WebAuthn\Config\RelyingParty;
16
use MadWizard\WebAuthn\Config\RelyingPartyInterface;
17
use MadWizard\WebAuthn\Credential\CredentialStoreInterface;
18
use MadWizard\WebAuthn\Exception\ConfigurationException;
19
use MadWizard\WebAuthn\Exception\UnsupportedException;
20
use MadWizard\WebAuthn\Extension\AppId\AppIdExtension;
21
use MadWizard\WebAuthn\Extension\ExtensionInterface;
22
use MadWizard\WebAuthn\Extension\ExtensionRegistry;
23
use MadWizard\WebAuthn\Extension\ExtensionRegistryInterface;
24
use MadWizard\WebAuthn\Metadata\MetadataResolver;
25
use MadWizard\WebAuthn\Metadata\MetadataResolverInterface;
26
use MadWizard\WebAuthn\Metadata\NullMetadataResolver;
27
use MadWizard\WebAuthn\Metadata\Provider\FileProvider;
28
use MadWizard\WebAuthn\Metadata\Provider\MetadataServiceProvider;
29
use MadWizard\WebAuthn\Metadata\Source\BundledSource;
30
use MadWizard\WebAuthn\Metadata\Source\MetadataServiceSource;
31
use MadWizard\WebAuthn\Metadata\Source\MetadataSourceInterface;
32
use MadWizard\WebAuthn\Metadata\Source\StatementDirectorySource;
33
use MadWizard\WebAuthn\Pki\CertificateStatusResolverInterface;
34
use MadWizard\WebAuthn\Pki\ChainValidator;
35
use MadWizard\WebAuthn\Pki\ChainValidatorInterface;
36
use MadWizard\WebAuthn\Pki\CrlCertificateStatusResolver;
37
use MadWizard\WebAuthn\Pki\NullCertificateStatusResolver;
38
use MadWizard\WebAuthn\Policy\Policy;
39
use MadWizard\WebAuthn\Policy\PolicyInterface;
40
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManager;
41
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManagerInterface;
42
use MadWizard\WebAuthn\Policy\Trust\Voter;
43
use MadWizard\WebAuthn\Remote\CachingClientFactory;
44
use MadWizard\WebAuthn\Remote\Downloader;
45
use MadWizard\WebAuthn\Remote\DownloaderInterface;
46
use MadWizard\WebAuthn\Server\ServerInterface;
47
use MadWizard\WebAuthn\Server\WebAuthnServer;
48
use Psr\Log\LoggerAwareInterface;
49
use Psr\Log\LoggerInterface;
50
use Psr\Log\NullLogger;
51
52
final class ServerBuilder
53
{
54
    /**
55
     * @var RelyingParty|null
56
     */
57
    private $rp;
58
59
    /**
60
     * @var CredentialStoreInterface|null
61
     */
62
    private $store;
63
64
    /**
65
     * @var string|null;
66
     */
67
    private $cacheDir;
68
69
    /**
70
     * @var callable|PolicyCallbackInterface|null
71
     */
72
    private $policyCallback;
73
74
    /**
75
     * @var MetadataSourceInterface[]
76
     */
77
    private $metadataSources = [];
78
79
    /**
80
     * @var LoggerInterface|null
81
     */
82
    private $logger;
83
84
    /**
85
     * @var bool
86
     */
87
    private $allowNoneAttestation = true;
88
89
    /**
90
     * @var bool
91
     */
92
    private $allowSelfAttestation = true;
93
94
    /**
95
     * @var bool
96
     */
97
    private $trustWithoutMetadata = true;
98
99
    /**
100
     * @var bool
101
     */
102
    private $validateUsingMetadata = true;
103
104
    /**
105
     * @var bool
106
     */
107
    private $strictSupportedFormats = false;
108
109
    /**
110
     * @var string[]
111
     */
112
    private $enabledExtensions = [];
113
114
    /**
115
     * @var ExtensionInterface[]
116
     */
117
    private $customExtensions = [];
118
119
    /**
120
     * @var bool
121
     */
122
    private $enableCrl = false;
123
124
    /**
125
     * @var bool
126
     */
127
    private $crlSilentFailure = true;
128
129
    private const SUPPORTED_EXTENSIONS = [
130
        'appid' => AppIdExtension::class,
131
    ];
132
133 19
    public function __construct()
134
    {
135 19
    }
136
137 19
    public function setRelyingParty(RelyingParty $rp): self
138
    {
139 19
        $this->rp = $rp;
140 19
        return $this;
141
    }
142
143 19
    public function setCredentialStore(CredentialStoreInterface $store): self
144
    {
145 19
        $this->store = $store;
146 19
        return $this;
147
    }
148
149
    public function setCacheDirectory(string $directory): self
150
    {
151
        $this->cacheDir = $directory;
152
        return $this;
153
    }
154
155
    public function useSystemTempCache(string $subDirectory = 'webauthn-server-cache'): self
156
    {
157
        $this->cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $subDirectory;
158
        return $this;
159
    }
160
161
    /**
162
     * @param callable|PolicyCallbackInterface $policyCallback
163
     *
164
     * @return $this
165
     */
166
    public function configurePolicy(callable $policyCallback): self
167
    {
168
        $this->policyCallback = $policyCallback;
169
        return $this;
170
    }
171
172
    /**
173
     * @return $this
174
     */
175
    public function allowNoneAttestation(bool $allow): self
176
    {
177
        $this->allowNoneAttestation = $allow;
178
        return $this;
179
    }
180
181
    /**
182
     * @return $this
183
     */
184
    public function strictSupportedFormats(bool $strict): self
185
    {
186
        $this->strictSupportedFormats = $strict;
187
        return $this;
188
    }
189
190
    /**
191
     * @return $this
192
     */
193
    public function validateUsingMetadata(bool $use): self
194
    {
195
        $this->validateUsingMetadata = $use;
196
        return $this;
197
    }
198
199
    /**
200
     * @return $this
201
     * @experimental
202
     */
203
    public function enableCrl(bool $enable, bool $silentFailure = true): self
204
    {
205
        if ($enable && !class_exists(\phpseclib3\File\X509::class)) {
206
            throw new UnsupportedException('CRL support requires phpseclib v3. Use composer require phpseclib/phpseclib ^3.0');
207
        }
208
        $this->enableCrl = $enable;
209
        $this->crlSilentFailure = $silentFailure;
210
        return $this;
211
    }
212
213
    /**
214
     * @return $this
215
     */
216
    public function allowSelfAttestation(bool $allow): self
217
    {
218
        $this->allowSelfAttestation = $allow;
219
        return $this;
220
    }
221
222
    /**
223
     * @return $this
224
     */
225
    public function trustWithoutMetadata(bool $trust): self
226
    {
227
        $this->trustWithoutMetadata = $trust;
228
        return $this;
229
    }
230
231
    /**
232
     * @return $this
233
     */
234
    public function enableExtensions(string ...$extensions): self
235
    {
236
        foreach ($extensions as $ext) {
237
            if (!isset(self::SUPPORTED_EXTENSIONS[$ext])) {
238
                throw new ConfigurationException(sprintf('Extension %s is not supported.', $ext));
239
            }
240
        }
241
        $this->enabledExtensions = array_merge($this->enabledExtensions, $extensions);
242
        return $this;
243
    }
244
245
    /**
246
     * @return $this
247
     */
248
    public function addCustomExtension(ExtensionInterface $extension): self
249
    {
250
        $this->customExtensions[] = $extension;
251
        return $this;
252
    }
253
254
    /**
255
     * @return $this
256
     */
257
    public function setLogger(LoggerInterface $logger): self
258
    {
259
        $this->logger = $logger;
260
        return $this;
261
    }
262
263
    private function assignLogger(LoggerAwareInterface $service): void
264
    {
265
        if ($this->logger !== null) {
266
            $service->setLogger($this->logger);
267
        }
268
    }
269
270 19
    public function build(): ServerInterface
271
    {
272 19
        $c = $this->setupContainer();
273
274 19
        return $c[ServerInterface::class];
275
    }
276
277 19
    private function setupContainer(): ServiceContainer
278
    {
279 19
        $c = new ServiceContainer();
280
281 19
        $this->setupConfiguredServices($c);
282 19
        $this->setupFormats($c);
283 19
        $this->setupTrustDecisionManager($c);
284 19
        $this->setupExtensions($c);
285
286
        $c[TrustPathValidatorInterface::class] = static function (ServiceContainer $c): TrustPathValidatorInterface {
287 19
            return new TrustPathValidator($c[ChainValidatorInterface::class]);
288
        };
289
290 19
        if ($this->enableCrl) {
291
            $this->setupCache($c);
292
            $this->setupDownloader($c);
293
            $c[CertificateStatusResolverInterface::class] = function (ServiceContainer $c): CertificateStatusResolverInterface {
294
                return new CrlCertificateStatusResolver($c[DownloaderInterface::class], $c[CacheProviderInterface::class], $this->crlSilentFailure);
295
            };
296
        } else {
297
            $c[CertificateStatusResolverInterface::class] = static function (): CertificateStatusResolverInterface {
298 19
                return new NullCertificateStatusResolver();
299
            };
300
        }
301
302
        $c[ChainValidatorInterface::class] = function (ServiceContainer $c): ChainValidatorInterface {
303 19
            return new ChainValidator($c[CertificateStatusResolverInterface::class]);
304
        };
305
306 19
        $c[PolicyInterface::class] = Closure::fromCallable([$this, 'createPolicy']);
307 19
        $c[MetadataResolverInterface::class] = Closure::fromCallable([$this, 'createMetadataResolver']);
308 19
        $c[ServerInterface::class] = Closure::fromCallable([$this, 'createServer']);
309
310 19
        return $c;
311
    }
312
313
    private function setupDownloader(ServiceContainer $c): void
314
    {
315
        $this->setupCache($c);
316
        if (isset($c[DownloaderInterface::class])) {
317
            return;
318
        }
319
        $c[DownloaderInterface::class] = static function (ServiceContainer $c): DownloaderInterface {
320
            return new Downloader($c[Client::class]);
321
        };
322
        $c[Client::class] = static function (ServiceContainer $c): Client {
323
            $factory = new CachingClientFactory($c[CacheProviderInterface::class]);
324
            return $factory->createClient();
325
        };
326
    }
327
328
    private function setupCache(ServiceContainer $c): void
329
    {
330
        if (isset($c[CacheProviderInterface::class])) {
331
            return;
332
        }
333
334
        $cacheDir = $this->cacheDir;
335
        if ($cacheDir === null) {
336
            throw new ConfigurationException('No cache directory configured. Use useCacheDirectory or useSystemTempCache.');
337
        }
338
        $c[CacheProviderInterface::class] = static function (ServiceContainer $c) use ($cacheDir): CacheProviderInterface {
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

338
        $c[CacheProviderInterface::class] = static function (/** @scrutinizer ignore-unused */ ServiceContainer $c) use ($cacheDir): CacheProviderInterface {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
339
            return new FileCacheProvider($cacheDir);
340
        };
341
    }
342
343 19
    private function setupConfiguredServices(ServiceContainer $c): void
344
    {
345 19
        if ($this->rp === null) {
346
            throw new ConfigurationException('Relying party not configured. Use setRelyingParty.');
347
        }
348
349
        $c[RelyingPartyInterface::class] = function (): RelyingPartyInterface { return $this->rp; };
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->rp could return the type null which is incompatible with the type-hinted return MadWizard\WebAuthn\Config\RelyingPartyInterface. Consider adding an additional type-check to rule them out.
Loading history...
350
351 19
        if ($this->store === null) {
352
            throw new ConfigurationException('Credential store not configured. Use setCredentialStore.');
353
        }
354
355
        $c[CredentialStoreInterface::class] = function (): CredentialStoreInterface { return $this->store; };
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->store could return the type null which is incompatible with the type-hinted return MadWizard\WebAuthn\Crede...redentialStoreInterface. Consider adding an additional type-check to rule them out.
Loading history...
356
        $c[LoggerInterface::class] = function (): LoggerInterface { return $this->logger ?? new NullLogger(); };
357 19
    }
358
359 19
    private function createPolicy(ServiceContainer $c): PolicyInterface
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

359
    private function createPolicy(/** @scrutinizer ignore-unused */ ServiceContainer $c): PolicyInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
360
    {
361 19
        $policy = new Policy();
362
363 19
        if ($this->policyCallback !== null) {
364
            ($this->policyCallback)($policy);
365
        }
366
367 19
        return $policy;
368
    }
369
370 19
    private function createServer(ServiceContainer $c): ServerInterface
371
    {
372 19
        return new WebAuthnServer(
373 19
            $c[RelyingPartyInterface::class],
374 19
            $c[PolicyInterface::class],
375 19
            $c[CredentialStoreInterface::class],
376 19
            $c[AttestationFormatRegistryInterface::class],
377 19
            $c[MetadataResolverInterface::class],
378 19
            $c[TrustDecisionManagerInterface::class],
379 19
            $c[ExtensionRegistryInterface::class]);
380
    }
381
382
    public function addMetadataSource(MetadataSourceInterface $metadataSource): self
383
    {
384
        $this->metadataSources[] = $metadataSource;
385
        return $this;
386
    }
387
388
    public function addBundledMetadataSource(array $sets = ['@all']): self
389
    {
390
        $this->metadataSources[] = new BundledSource($sets);
391
        return $this;
392
    }
393
394 19
    private function createMetadataResolver(ServiceContainer $c): MetadataResolverInterface
395
    {
396 19
        if (count($this->metadataSources) === 0) {
397 19
            return new NullMetadataResolver();
398
        }
399
        return new MetadataResolver($this->createMetadataProviders($c));
400
    }
401
402 19
    private function setupTrustDecisionManager(ServiceContainer $c): void
403
    {
404
        $c[TrustDecisionManagerInterface::class] = function (ServiceContainer $c): TrustDecisionManagerInterface {
405 19
            $tdm = new TrustDecisionManager();
406
407 19
            if ($this->allowNoneAttestation) {
408 19
                $tdm->addVoter(new Voter\TrustAttestationTypeVoter(AttestationType::NONE));
409
            }
410 19
            if ($this->allowSelfAttestation) {
411 19
                $tdm->addVoter(new Voter\TrustAttestationTypeVoter(AttestationType::SELF));
412
            }
413 19
            if ($this->trustWithoutMetadata) {
414 19
                $tdm->addVoter(new Voter\AllowEmptyMetadataVoter());
415
            }
416 19
            if ($this->validateUsingMetadata) {
417 19
                $tdm->addVoter(new Voter\SupportedAttestationTypeVoter());
418 19
                $tdm->addVoter(new Voter\UndesiredStatusReportVoter());
419 19
                $tdm->addVoter(new Voter\TrustChainVoter($c[TrustPathValidatorInterface::class]));
420
            }
421 19
            return $tdm;
422
        };
423 19
    }
424
425
    private function createMetadataProviders(ServiceContainer $c): array
426
    {
427
        $providers = [];
428
        foreach ($this->metadataSources as $source) {
429
            // TODO: More elegant solution than if/else
430
            if ($source instanceof StatementDirectorySource) {
431
                $providers[] = new FileProvider($source);
432
            } elseif ($source instanceof MetadataServiceSource) {
433
                $this->setupDownloader($c);
434
                $providers[] = new MetadataServiceProvider($source, $c[DownloaderInterface::class], $c[CacheProviderInterface::class], $c[ChainValidatorInterface::class]);
435
            } elseif ($source instanceof BundledSource) {
436
                $providers = array_merge(
437
                    $providers,
438
                    $source->createProviders()
439
                );
440
            } else {
441
                throw new UnsupportedException(sprintf('No provider available for metadata source of type %s.', get_class($source)));
442
            }
443
        }
444
445
        foreach ($providers as $provider) {
446
            if ($provider instanceof LoggerAwareInterface) {
447
                $this->assignLogger($provider);
448
            }
449
        }
450
        return $providers;
451
    }
452
453 19
    private function setupFormats(ServiceContainer $c): void
454
    {
455
        $c[Verifier\PackedAttestationVerifier::class] = static function (): Verifier\PackedAttestationVerifier {
456 19
            return new Verifier\PackedAttestationVerifier();
457
        };
458
        $c[Verifier\FidoU2fAttestationVerifier::class] = static function (): Verifier\FidoU2fAttestationVerifier {
459 19
            return new Verifier\FidoU2fAttestationVerifier();
460
        };
461
        $c[Verifier\NoneAttestationVerifier::class] = static function (): Verifier\NoneAttestationVerifier {
462 19
            return new Verifier\NoneAttestationVerifier();
463
        };
464
        $c[Verifier\TpmAttestationVerifier::class] = static function (): Verifier\TpmAttestationVerifier {
465 19
            return new Verifier\TpmAttestationVerifier();
466
        };
467
        $c[Verifier\AndroidSafetyNetAttestationVerifier::class] = static function (): Verifier\AndroidSafetyNetAttestationVerifier {
468 19
            return new Verifier\AndroidSafetyNetAttestationVerifier();
469
        };
470
        $c[Verifier\AndroidKeyAttestationVerifier::class] = static function (): Verifier\AndroidKeyAttestationVerifier {
471 19
            return new Verifier\AndroidKeyAttestationVerifier();
472
        };
473
        $c[Verifier\AppleAttestationVerifier::class] = static function (): Verifier\AppleAttestationVerifier {
474 19
            return new Verifier\AppleAttestationVerifier();
475
        };
476
477
        $c[AttestationFormatRegistryInterface::class] = function (ServiceContainer $c): AttestationFormatRegistryInterface {
478 19
            $registry = new AttestationFormatRegistry();
479
480 19
            $registry->strictSupportedFormats($this->strictSupportedFormats);
481
482 19
            $registry->addFormat($c[Verifier\PackedAttestationVerifier::class]->getSupportedFormat());
483 19
            $registry->addFormat($c[Verifier\FidoU2fAttestationVerifier::class]->getSupportedFormat());
484 19
            $registry->addFormat($c[Verifier\NoneAttestationVerifier::class]->getSupportedFormat());
485 19
            $registry->addFormat($c[Verifier\TpmAttestationVerifier::class]->getSupportedFormat());
486 19
            $registry->addFormat($c[Verifier\AndroidSafetyNetAttestationVerifier::class]->getSupportedFormat());
487 19
            $registry->addFormat($c[Verifier\AndroidKeyAttestationVerifier::class]->getSupportedFormat());
488 19
            $registry->addFormat($c[Verifier\AppleAttestationVerifier::class]->getSupportedFormat());
489
490 19
            return $registry;
491
        };
492 19
    }
493
494 19
    private function setupExtensions(ServiceContainer $c): void
495
    {
496
        $c[AppIdExtension::class] = static function (ServiceContainer $c): AppIdExtension {
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

496
        $c[AppIdExtension::class] = static function (/** @scrutinizer ignore-unused */ ServiceContainer $c): AppIdExtension {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
497
            return new AppIdExtension();
498
        };
499
        $c[ExtensionRegistryInterface::class] = function (ServiceContainer $c): ExtensionRegistryInterface {
500 19
            $registry = new ExtensionRegistry();
501 19
            foreach (array_unique($this->enabledExtensions) as $ext) {
502
                $registry->addExtension($c[self::SUPPORTED_EXTENSIONS[$ext]]);
503
            }
504 19
            foreach ($this->customExtensions as $ext) {
505
                $registry->addExtension($ext);
506
            }
507 19
            return $registry;
508
        };
509 19
    }
510
}
511