Passed
Push — master ( f5ca5a...0e4678 )
by Thomas
07:23
created

ServerBuilder::enableCrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 8
ccs 0
cts 6
cp 0
crap 6
rs 10
c 0
b 0
f 0
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\MetadataServiceSource;
30
use MadWizard\WebAuthn\Metadata\Source\MetadataSourceInterface;
31
use MadWizard\WebAuthn\Metadata\Source\StatementDirectorySource;
32
use MadWizard\WebAuthn\Pki\CertificateStatusResolverInterface;
33
use MadWizard\WebAuthn\Pki\ChainValidator;
34
use MadWizard\WebAuthn\Pki\ChainValidatorInterface;
35
use MadWizard\WebAuthn\Pki\CrlCertificateStatusResolver;
36
use MadWizard\WebAuthn\Pki\NullCertificateStatusResolver;
37
use MadWizard\WebAuthn\Policy\Policy;
38
use MadWizard\WebAuthn\Policy\PolicyInterface;
39
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManager;
40
use MadWizard\WebAuthn\Policy\Trust\TrustDecisionManagerInterface;
41
use MadWizard\WebAuthn\Policy\Trust\Voter;
42
use MadWizard\WebAuthn\Remote\CachingClientFactory;
43
use MadWizard\WebAuthn\Remote\Downloader;
44
use MadWizard\WebAuthn\Remote\DownloaderInterface;
45
use MadWizard\WebAuthn\Server\ServerInterface;
46
use MadWizard\WebAuthn\Server\WebAuthnServer;
47
use Psr\Log\LoggerAwareInterface;
48
use Psr\Log\LoggerInterface;
49
use Psr\Log\NullLogger;
50
51
final class ServerBuilder
52
{
53
    /**
54
     * @var RelyingParty|null
55
     */
56
    private $rp;
57
58
    /**
59
     * @var CredentialStoreInterface|null
60
     */
61
    private $store;
62
63
    /**
64
     * @var string|null;
65
     */
66
    private $cacheDir;
67
68
    /**
69
     * @var callable|PolicyCallbackInterface|null
70
     */
71
    private $policyCallback;
72
73
    /**
74
     * @var MetadataSourceInterface[]
75
     */
76
    private $metadataSources = [];
77
78
    /**
79
     * @var LoggerInterface|null
80
     */
81
    private $logger;
82
83
    /**
84
     * @var bool
85
     */
86
    private $allowNoneAttestation = true;
87
88
    /**
89
     * @var bool
90
     */
91
    private $allowSelfAttestation = true;
92
93
    /**
94
     * @var bool
95
     */
96
    private $trustWithoutMetadata = true;
97
98
    /**
99
     * @var bool
100
     */
101
    private $useMetadata = true;
102
103
    /**
104
     * @var bool
105
     */
106
    private $strictSupportedFormats = false;
107
108
    /**
109
     * @var string[]
110
     */
111
    private $enabledExtensions = [];
112
113
    /**
114
     * @var ExtensionInterface[]
115
     */
116
    private $customExtensions = [];
117
118
    /**
119
     * @var bool
120
     */
121
    private $enableCrl = false;
122
123
    /**
124
     * @var bool
125
     */
126
    private $crlSilentFailure = true;
127
128
    private const SUPPORTED_EXTENSIONS = [
129
        'appid' => AppIdExtension::class,
130
    ];
131
132 18
    public function __construct()
133
    {
134 18
    }
135
136 18
    public function setRelyingParty(RelyingParty $rp): self
137
    {
138 18
        $this->rp = $rp;
139 18
        return $this;
140
    }
141
142 18
    public function setCredentialStore(CredentialStoreInterface $store): self
143
    {
144 18
        $this->store = $store;
145 18
        return $this;
146
    }
147
148
    public function setCacheDirectory(string $directory): self
149
    {
150
        $this->cacheDir = $directory;
151
        return $this;
152
    }
153
154
    public function useSystemTempCache(string $subDirectory = 'webauthn-server-cache'): self
155
    {
156
        $this->cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $subDirectory;
157
        return $this;
158
    }
159
160
    /**
161
     * @param callable|PolicyCallbackInterface $policyCallback
162
     *
163
     * @return $this
164
     */
165
    public function configurePolicy(callable $policyCallback): self
166
    {
167
        $this->policyCallback = $policyCallback;
168
        return $this;
169
    }
170
171
    /**
172
     * @return $this
173
     */
174
    public function allowNoneAttestation(bool $allow): self
175
    {
176
        $this->allowNoneAttestation = $allow;
177
        return $this;
178
    }
179
180
    /**
181
     * @return $this
182
     */
183
    public function strictSupportedFormats(bool $strict): self
184
    {
185
        $this->strictSupportedFormats = $strict;
186
        return $this;
187
    }
188
189
    /**
190
     * @return $this
191
     */
192
    public function useMetadata(bool $use): self
193
    {
194
        $this->useMetadata = $use;
195
        return $this;
196
    }
197
198
    /**
199
     * @return $this
200
     * @experimental
201
     */
202
    public function enableCrl(bool $enable, bool $silentFailure = true)
0 ignored issues
show
Unused Code introduced by
The parameter $enable 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

202
    public function enableCrl(/** @scrutinizer ignore-unused */ bool $enable, bool $silentFailure = true)

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...
203
    {
204
        if (!class_exists(\phpseclib3\File\X509::class)) {
205
            throw new UnsupportedException('CRL support is experimental and requires a (not yet stable) phpseclib v3. Use composer require phpseclib/phpseclib 3.0.x-dev.');
206
        }
207
        $this->enableCrl = true;
208
        $this->crlSilentFailure = $silentFailure;
209
        return $this;
210
    }
211
212
    /**
213
     * @return $this
214
     */
215
    public function allowSelfAttestation(bool $allow): self
216
    {
217
        $this->allowSelfAttestation = $allow;
218
        return $this;
219
    }
220
221
    /**
222
     * @return $this
223
     */
224
    public function trustWithoutMetadata(bool $trust): self
225
    {
226
        $this->trustWithoutMetadata = $trust;
227
        return $this;
228
    }
229
230
    /**
231
     * @return $this
232
     */
233
    public function enableExtensions(string ...$extensions): self
234
    {
235
        foreach ($extensions as $ext) {
236
            if (!isset(self::SUPPORTED_EXTENSIONS[$ext])) {
237
                throw new ConfigurationException(sprintf('Extension %s is not supported.', $ext));
238
            }
239
        }
240
        $this->enabledExtensions = array_merge($this->enabledExtensions, $extensions);
241
        return $this;
242
    }
243
244
    /**
245
     * @return $this
246
     */
247
    public function addCustomExtension(ExtensionInterface $extension): self
248
    {
249
        $this->customExtensions[] = $extension;
250
        return $this;
251
    }
252
253
    /**
254
     * @return $this
255
     */
256
    public function setLogger(LoggerInterface $logger): self
257
    {
258
        $this->logger = $logger;
259
        return $this;
260
    }
261
262
    private function assignLogger(LoggerAwareInterface $service): void
263
    {
264
        if ($this->logger !== null) {
265
            $service->setLogger($this->logger);
266
        }
267
    }
268
269 18
    public function build(): ServerInterface
270
    {
271 18
        $c = $this->setupContainer();
272
273 18
        return $c[ServerInterface::class];
274
    }
275
276 18
    private function setupContainer(): ServiceContainer
277
    {
278 18
        $c = new ServiceContainer();
279
280 18
        $this->setupConfiguredServices($c);
281 18
        $this->setupFormats($c);
282 18
        $this->setupTrustDecisionManager($c);
283 18
        $this->setupExtensions($c);
284
285
        $c[TrustPathValidatorInterface::class] = static function (ServiceContainer $c): TrustPathValidatorInterface {
286 18
            return new TrustPathValidator($c[ChainValidatorInterface::class]);
287
        };
288
289 18
        if ($this->enableCrl) {
290
            $this->setupCache($c);
291
            $this->setupDownloader($c);
292
            $c[CertificateStatusResolverInterface::class] = function (ServiceContainer $c): CertificateStatusResolverInterface {
293
                return new CrlCertificateStatusResolver($c[DownloaderInterface::class], $c[CacheProviderInterface::class], $this->crlSilentFailure);
294
            };
295
        } else {
296
            $c[CertificateStatusResolverInterface::class] = static function (ServiceContainer $c): CertificateStatusResolverInterface {
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

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

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...
297 18
                return new NullCertificateStatusResolver();
298
            };
299
        }
300
301
        $c[ChainValidatorInterface::class] = function (ServiceContainer $c): ChainValidatorInterface {
302 18
            return new ChainValidator($c[CertificateStatusResolverInterface::class]);
303
        };
304
305 18
        $c[PolicyInterface::class] = Closure::fromCallable([$this, 'createPolicy']);
306 18
        $c[MetadataResolverInterface::class] = Closure::fromCallable([$this, 'createMetadataResolver']);
307 18
        $c[ServerInterface::class] = Closure::fromCallable([$this, 'createServer']);
308
309 18
        return $c;
310
    }
311
312
    private function setupDownloader(ServiceContainer $c)
313
    {
314
        $this->setupCache($c);
315
        if (isset($c[DownloaderInterface::class])) {
316
            return;
317
        }
318
        $c[DownloaderInterface::class] = static function (ServiceContainer $c): DownloaderInterface {
319
            return new Downloader($c[Client::class]);
320
        };
321
        $c[Client::class] = static function (ServiceContainer $c): Client {
322
            $factory = new CachingClientFactory($c[CacheProviderInterface::class]);
323
            return $factory->createClient();
324
        };
325
    }
326
327
    private function setupCache(ServiceContainer $c)
328
    {
329
        if (isset($c[CacheProviderInterface::class])) {
330
            return;
331
        }
332
333
        $cacheDir = $this->cacheDir;
334
        if ($cacheDir === null) {
335
            throw new ConfigurationException('No cache directory configured. Use useCacheDirectory or useSystemTempCache.');
336
        }
337
        $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

337
        $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...
338
            return new FileCacheProvider($cacheDir);
339
        };
340
    }
341
342 18
    private function setupConfiguredServices(ServiceContainer $c): void
343
    {
344 18
        if ($this->rp === null) {
345
            throw new ConfigurationException('Relying party not configured. Use setRelyingParty.');
346
        }
347
348
        $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...
349
350 18
        if ($this->store === null) {
351
            throw new ConfigurationException('Credential store not configured. Use setCredentialStore.');
352
        }
353
354
        $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...
355
        $c[LoggerInterface::class] = function (): LoggerInterface { return $this->logger ?? new NullLogger(); };
356 18
    }
357
358 18
    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

358
    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...
359
    {
360 18
        $policy = new Policy();
361
362 18
        if ($this->policyCallback !== null) {
363
            ($this->policyCallback)($policy);
364
        }
365
366 18
        return $policy;
367
    }
368
369 18
    private function createServer(ServiceContainer $c): ServerInterface
370
    {
371 18
        return new WebAuthnServer(
372 18
            $c[RelyingPartyInterface::class],
373 18
            $c[PolicyInterface::class],
374 18
            $c[CredentialStoreInterface::class],
375 18
            $c[AttestationFormatRegistryInterface::class],
376 18
            $c[MetadataResolverInterface::class],
377 18
            $c[TrustDecisionManagerInterface::class],
378 18
            $c[ExtensionRegistryInterface::class]);
379
    }
380
381
    public function addMetadataSource(MetadataSourceInterface $metadataSource): self
382
    {
383
        $this->metadataSources[] = $metadataSource;
384
        return $this;
385
    }
386
387 18
    private function createMetadataResolver(ServiceContainer $c): MetadataResolverInterface
388
    {
389 18
        if (count($this->metadataSources) === 0) {
390 18
            return new NullMetadataResolver();
391
        }
392
        return new MetadataResolver($this->createMetadataProviders($c));
393
    }
394
395 18
    private function setupTrustDecisionManager(ServiceContainer $c)
396
    {
397
        $c[TrustDecisionManagerInterface::class] = function (ServiceContainer $c): TrustDecisionManagerInterface {
398 18
            $tdm = new TrustDecisionManager();
399
400 18
            if ($this->allowNoneAttestation) {
401 18
                $tdm->addVoter(new Voter\TrustAttestationTypeVoter(AttestationType::NONE));
402
            }
403 18
            if ($this->allowSelfAttestation) {
404 18
                $tdm->addVoter(new Voter\TrustAttestationTypeVoter(AttestationType::SELF));
405
            }
406 18
            if ($this->trustWithoutMetadata) {
407 18
                $tdm->addVoter(new Voter\AllowEmptyMetadataVoter());
408
            }
409 18
            if ($this->useMetadata) {
410 18
                $tdm->addVoter(new Voter\SupportedAttestationTypeVoter());
411 18
                $tdm->addVoter(new Voter\UndesiredStatusReportVoter());
412 18
                $tdm->addVoter(new Voter\TrustChainVoter($c[TrustPathValidatorInterface::class]));
413
            }
414 18
            return $tdm;
415
        };
416 18
    }
417
418
    private function createMetadataProviders(ServiceContainer $c): array
419
    {
420
        $providers = [];
421
        foreach ($this->metadataSources as $source) {
422
            if ($source instanceof StatementDirectorySource) {
423
                $provider = new FileProvider($source);
424
            } elseif ($source instanceof MetadataServiceSource) {
425
                $this->setupDownloader($c);
426
                $provider = new MetadataServiceProvider($source, $c[DownloaderInterface::class], $c[CacheProviderInterface::class], $c[ChainValidatorInterface::class]);
427
            } else {
428
                throw new UnsupportedException(sprintf('No provider available for metadata source of type %s.', get_class($source)));
429
            }
430
431
            if ($provider instanceof LoggerAwareInterface) {
432
                $this->assignLogger($provider);
433
            }
434
            $providers[] = $provider;
435
        }
436
        return $providers;
437
    }
438
439 18
    private function setupFormats(ServiceContainer $c)
440
    {
441
        $c[Verifier\PackedAttestationVerifier::class] = static function (): Verifier\PackedAttestationVerifier {
442 18
            return new Verifier\PackedAttestationVerifier();
443
        };
444
        $c[Verifier\FidoU2fAttestationVerifier::class] = static function (): Verifier\FidoU2fAttestationVerifier {
445 18
            return new Verifier\FidoU2fAttestationVerifier();
446
        };
447
        $c[Verifier\NoneAttestationVerifier::class] = static function (): Verifier\NoneAttestationVerifier {
448 18
            return new Verifier\NoneAttestationVerifier();
449
        };
450
        $c[Verifier\TpmAttestationVerifier::class] = static function (): Verifier\TpmAttestationVerifier {
451 18
            return new Verifier\TpmAttestationVerifier();
452
        };
453
        $c[Verifier\AndroidSafetyNetAttestationVerifier::class] = static function (): Verifier\AndroidSafetyNetAttestationVerifier {
454 18
            return new Verifier\AndroidSafetyNetAttestationVerifier();
455
        };
456
        $c[Verifier\AndroidKeyAttestationVerifier::class] = static function (): Verifier\AndroidKeyAttestationVerifier {
457 18
            return new Verifier\AndroidKeyAttestationVerifier();
458
        };
459
460
        $c[AttestationFormatRegistryInterface::class] = function (ServiceContainer $c): AttestationFormatRegistryInterface {
461 18
            $registry = new AttestationFormatRegistry();
462
463 18
            $registry->strictSupportedFormats($this->strictSupportedFormats);
464
465 18
            $registry->addFormat($c[Verifier\PackedAttestationVerifier::class]->getSupportedFormat());
466 18
            $registry->addFormat($c[Verifier\FidoU2fAttestationVerifier::class]->getSupportedFormat());
467 18
            $registry->addFormat($c[Verifier\NoneAttestationVerifier::class]->getSupportedFormat());
468 18
            $registry->addFormat($c[Verifier\TpmAttestationVerifier::class]->getSupportedFormat());
469 18
            $registry->addFormat($c[Verifier\AndroidSafetyNetAttestationVerifier::class]->getSupportedFormat());
470 18
            $registry->addFormat($c[Verifier\AndroidKeyAttestationVerifier::class]->getSupportedFormat());
471
472 18
            return $registry;
473
        };
474 18
    }
475
476 18
    private function setupExtensions(ServiceContainer $c): void
477
    {
478
        $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

478
        $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...
479
            return new AppIdExtension();
480
        };
481
        $c[ExtensionRegistryInterface::class] = function (ServiceContainer $c): ExtensionRegistryInterface {
482 18
            $registry = new ExtensionRegistry();
483 18
            foreach (array_unique($this->enabledExtensions) as $ext) {
484
                $registry->addExtension($c[self::SUPPORTED_EXTENSIONS[$ext]]);
485
            }
486 18
            foreach ($this->customExtensions as $ext) {
487
                $registry->addExtension($ext);
488
            }
489 18
            return $registry;
490
        };
491 18
    }
492
}
493