GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

PathValidator   F
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 596
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 75
eloc 172
dl 0
loc 596
ccs 196
cts 196
cp 1
rs 2.4
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A _prepareNameConstraints() 0 8 2
A __construct() 0 14 3
B _processCertificate() 0 35 7
A _preparePolicyMappings() 0 15 4
A _calculatePolicyIntersection() 0 18 3
A _processExtensions() 0 5 1
A _checkExcludedSubtrees() 0 4 1
A _checkKeyUsage() 0 7 3
A _prepareInhibitAnyPolicy() 0 11 3
A _processNameConstraints() 0 5 1
A _processPathLengthContraint() 0 13 4
A _checkIssuer() 0 4 2
B _wrapUp() 0 29 7
A _checkValidity() 0 10 3
A _prepareNonSelfIssued() 0 15 4
A _processBasicContraints() 0 12 4
A _checkPermittedSubtrees() 0 4 1
A validate() 0 22 4
A _verifySignature() 0 12 3
A _prepareNext() 0 30 2
A _verifyMaxPathLength() 0 11 3
A _checkRevocation() 0 2 1
A _setPublicKeyState() 0 21 3
A _preparePolicyConstraints() 0 19 6

How to fix   Complexity   

Complex Class

Complex classes like PathValidator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PathValidator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Sop\X509\CertificationPath\PathValidation;
6
7
use Sop\CryptoBridge\Crypto;
8
use Sop\X509\Certificate\Certificate;
9
use Sop\X509\Certificate\Extension\CertificatePolicy\PolicyInformation;
10
use Sop\X509\Certificate\TBSCertificate;
11
use Sop\X509\CertificationPath\Exception\PathValidationException;
12
13
/**
14
 * Implements certification path validation.
15
 *
16
 * @see https://tools.ietf.org/html/rfc5280#section-6
17
 */
18
class PathValidator
19
{
20
    /**
21
     * Crypto engine.
22
     *
23
     * @var Crypto
24
     */
25
    protected $_crypto;
26
27
    /**
28
     * Path validation configuration.
29
     *
30
     * @var PathValidationConfig
31
     */
32
    protected $_config;
33
34
    /**
35
     * Certification path.
36
     *
37
     * @var Certificate[]
38
     */
39
    protected $_certificates;
40
41
    /**
42
     * Certification path trust anchor.
43
     *
44
     * @var Certificate
45
     */
46
    protected $_trustAnchor;
47
48
    /**
49
     * Constructor.
50
     *
51
     * @param Crypto               $crypto          Crypto engine
52
     * @param PathValidationConfig $config          Validation config
53
     * @param Certificate          ...$certificates Certificates from the trust anchor to
54
     *                                              the end-entity certificate
55
     */
56 46
    public function __construct(Crypto $crypto, PathValidationConfig $config,
57
        Certificate ...$certificates)
58
    {
59 46
        if (!count($certificates)) {
60 1
            throw new \LogicException('No certificates.');
61
        }
62 45
        $this->_crypto = $crypto;
63 45
        $this->_config = $config;
64 45
        $this->_certificates = $certificates;
65
        // if trust anchor is explicitly given in configuration
66 45
        if ($config->hasTrustAnchor()) {
67 1
            $this->_trustAnchor = $config->trustAnchor();
68
        } else {
69 44
            $this->_trustAnchor = $certificates[0];
70
        }
71 45
    }
72
73
    /**
74
     * Validate certification path.
75
     *
76
     * @throws PathValidationException
77
     *
78
     * @return PathValidationResult
79
     */
80 45
    public function validate(): PathValidationResult
81
    {
82 45
        $n = count($this->_certificates);
83 45
        $state = ValidatorState::initialize(
84 45
            $this->_config, $this->_trustAnchor, $n);
85 45
        for ($i = 0; $i < $n; ++$i) {
86 44
            $state = $state->withIndex($i + 1);
87 44
            $cert = $this->_certificates[$i];
88
            // process certificate (section 6.1.3.)
89 44
            $state = $this->_processCertificate($state, $cert);
90 43
            if (!$state->isFinal()) {
91
                // prepare next certificate (section 6.1.4.)
92 43
                $state = $this->_prepareNext($state, $cert);
93
            }
94
        }
95 31
        if (!isset($cert)) {
96 1
            throw new \LogicException('No certificates.');
97
        }
98
        // wrap-up (section 6.1.5.)
99 30
        $state = $this->_wrapUp($state, $cert);
100
        // return outputs
101 28
        return $state->getResult($this->_certificates);
102
    }
103
104
    /**
105
     * Apply basic certificate processing according to RFC 5280 section 6.1.3.
106
     *
107
     * @see https://tools.ietf.org/html/rfc5280#section-6.1.3
108
     *
109
     * @param ValidatorState $state
110
     * @param Certificate    $cert
111
     *
112
     * @throws PathValidationException
113
     *
114
     * @return ValidatorState
115
     */
116 44
    private function _processCertificate(ValidatorState $state,
117
        Certificate $cert): ValidatorState
118
    {
119
        // (a.1) verify signature
120 44
        $this->_verifySignature($state, $cert);
121
        // (a.2) check validity period
122 44
        $this->_checkValidity($cert);
123
        // (a.3) check that certificate is not revoked
124 43
        $this->_checkRevocation($cert);
125
        // (a.4) check issuer
126 43
        $this->_checkIssuer($state, $cert);
127
        // (b)(c) if certificate is self-issued and it is not
128
        // the final certificate in the path, skip this step
129 43
        if (!($cert->isSelfIssued() && !$state->isFinal())) {
130
            // (b) check permitted subtrees
131 35
            $this->_checkPermittedSubtrees($state, $cert);
132
            // (c) check excluded subtrees
133 35
            $this->_checkExcludedSubtrees($state, $cert);
134
        }
135 43
        $extensions = $cert->tbsCertificate()->extensions();
136 43
        if ($extensions->hasCertificatePolicies()) {
137
            // (d) process policy information
138 26
            if ($state->hasValidPolicyTree()) {
139 26
                $state = $state->validPolicyTree()->processPolicies($state, $cert);
140
            }
141
        } else {
142
            // (e) certificate policies extension not present,
143
            // set the valid_policy_tree to NULL
144 31
            $state = $state->withoutValidPolicyTree();
145
        }
146
        // (f) check that explicit_policy > 0 or valid_policy_tree is set
147 43
        if (!($state->explicitPolicy() > 0 || $state->hasValidPolicyTree())) {
148 3
            throw new PathValidationException('No valid policies.');
149
        }
150 43
        return $state;
151
    }
152
153
    /**
154
     * Apply preparation for the certificate i+1 according to rfc5280 section
155
     * 6.1.4.
156
     *
157
     * @see https://tools.ietf.org/html/rfc5280#section-6.1.4
158
     *
159
     * @param ValidatorState $state
160
     * @param Certificate    $cert
161
     *
162
     * @return ValidatorState
163
     */
164 43
    private function _prepareNext(ValidatorState $state,
165
        Certificate $cert): ValidatorState
166
    {
167
        // (a)(b) if policy mappings extension is present
168 43
        $state = $this->_preparePolicyMappings($state, $cert);
169
        // (c) assign working_issuer_name
170 42
        $state = $state->withWorkingIssuerName(
171 42
            $cert->tbsCertificate()->subject());
172
        // (d)(e)(f)
173 42
        $state = $this->_setPublicKeyState($state, $cert);
174
        // (g) if name constraints extension is present
175 42
        $state = $this->_prepareNameConstraints($state, $cert);
176
        // (h) if certificate is not self-issued
177 42
        if (!$cert->isSelfIssued()) {
178 21
            $state = $this->_prepareNonSelfIssued($state);
179
        }
180
        // (i) if policy constraints extension is present
181 42
        $state = $this->_preparePolicyConstraints($state, $cert);
182
        // (j) if inhibit any policy extension is present
183 42
        $state = $this->_prepareInhibitAnyPolicy($state, $cert);
184
        // (k) check basic constraints
185 42
        $this->_processBasicContraints($cert);
186
        // (l) verify max_path_length
187 40
        $state = $this->_verifyMaxPathLength($state, $cert);
188
        // (m) check pathLenContraint
189 40
        $state = $this->_processPathLengthContraint($state, $cert);
190
        // (n) check key usage
191 40
        $this->_checkKeyUsage($cert);
192
        // (o) process relevant extensions
193 39
        return $this->_processExtensions($state, $cert);
194
    }
195
196
    /**
197
     * Apply wrap-up procedure according to RFC 5280 section 6.1.5.
198
     *
199
     * @see https://tools.ietf.org/html/rfc5280#section-6.1.5
200
     *
201
     * @param ValidatorState $state
202
     * @param Certificate    $cert
203
     *
204
     * @throws PathValidationException
205
     *
206
     * @return ValidatorState
207
     */
208 30
    private function _wrapUp(ValidatorState $state,
209
        Certificate $cert): ValidatorState
210
    {
211 30
        $tbs_cert = $cert->tbsCertificate();
212 30
        $extensions = $tbs_cert->extensions();
213
        // (a)
214 30
        if ($state->explicitPolicy() > 0) {
215 26
            $state = $state->withExplicitPolicy($state->explicitPolicy() - 1);
216
        }
217
        // (b)
218 30
        if ($extensions->hasPolicyConstraints()) {
219 13
            $ext = $extensions->policyConstraints();
220 13
            if ($ext->hasRequireExplicitPolicy() &&
221 13
                0 === $ext->requireExplicitPolicy()) {
222 1
                $state = $state->withExplicitPolicy(0);
223
            }
224
        }
225
        // (c)(d)(e)
226 30
        $state = $this->_setPublicKeyState($state, $cert);
227
        // (f) process relevant extensions
228 30
        $state = $this->_processExtensions($state, $cert);
229
        // (g) intersection of valid_policy_tree and the initial-policy-set
230 30
        $state = $this->_calculatePolicyIntersection($state);
231
        // check that explicit_policy > 0 or valid_policy_tree is set
232 30
        if (!($state->explicitPolicy() > 0 || $state->hasValidPolicyTree())) {
233 2
            throw new PathValidationException('No valid policies.');
234
        }
235
        // path validation succeeded
236 28
        return $state;
237
    }
238
239
    /**
240
     * Update working_public_key, working_public_key_parameters and
241
     * working_public_key_algorithm state variables from certificate.
242
     *
243
     * @param ValidatorState $state
244
     * @param Certificate    $cert
245
     *
246
     * @return ValidatorState
247
     */
248 42
    private function _setPublicKeyState(ValidatorState $state,
249
        Certificate $cert): ValidatorState
250
    {
251 42
        $pk_info = $cert->tbsCertificate()->subjectPublicKeyInfo();
252
        // assign working_public_key
253 42
        $state = $state->withWorkingPublicKey($pk_info);
254
        // assign working_public_key_parameters
255 42
        $params = ValidatorState::getAlgorithmParameters(
256 42
            $pk_info->algorithmIdentifier());
257 42
        if (null !== $params) {
258 42
            $state = $state->withWorkingPublicKeyParameters($params);
259
        } else {
260
            // if algorithms differ, set parameters to null
261 1
            if ($pk_info->algorithmIdentifier()->oid() !==
262 1
                    $state->workingPublicKeyAlgorithm()->oid()) {
263 1
                $state = $state->withWorkingPublicKeyParameters(null);
264
            }
265
        }
266
        // assign working_public_key_algorithm
267 42
        return $state->withWorkingPublicKeyAlgorithm(
268 42
            $pk_info->algorithmIdentifier());
269
    }
270
271
    /**
272
     * Verify certificate signature.
273
     *
274
     * @param ValidatorState $state
275
     * @param Certificate    $cert
276
     *
277
     * @throws PathValidationException
278
     */
279 44
    private function _verifySignature(ValidatorState $state,
280
        Certificate $cert): void
281
    {
282
        try {
283 44
            $valid = $cert->verify($state->workingPublicKey(), $this->_crypto);
284 1
        } catch (\RuntimeException $e) {
285 1
            throw new PathValidationException(
286 1
                'Failed to verify signature: ' . $e->getMessage(), 0, $e);
287
        }
288 44
        if (!$valid) {
289 2
            throw new PathValidationException(
290 2
                "Certificate signature doesn't match.");
291
        }
292 44
    }
293
294
    /**
295
     * Check certificate validity.
296
     *
297
     * @param Certificate $cert
298
     *
299
     * @throws PathValidationException
300
     */
301 44
    private function _checkValidity(Certificate $cert): void
302
    {
303 44
        $refdt = $this->_config->dateTime();
304 44
        $validity = $cert->tbsCertificate()->validity();
305 44
        if ($validity->notBefore()->dateTime()->diff($refdt)->invert) {
306 1
            throw new PathValidationException(
307 1
                'Certificate validity period has not started.');
308
        }
309 43
        if ($refdt->diff($validity->notAfter()->dateTime())->invert) {
310 1
            throw new PathValidationException('Certificate has expired.');
311
        }
312 43
    }
313
314
    /**
315
     * Check certificate revocation.
316
     *
317
     * @param Certificate $cert
318
     */
319 43
    private function _checkRevocation(Certificate $cert)
320
    {
321
        // @todo Implement CRL handling
322 43
    }
323
324
    /**
325
     * Check certificate issuer.
326
     *
327
     * @param ValidatorState $state
328
     * @param Certificate    $cert
329
     *
330
     * @throws PathValidationException
331
     */
332 43
    private function _checkIssuer(ValidatorState $state, Certificate $cert): void
333
    {
334 43
        if (!$cert->tbsCertificate()->issuer()->equals($state->workingIssuerName())) {
335 1
            throw new PathValidationException('Certification issuer mismatch.');
336
        }
337 43
    }
338
339
    /**
340
     * @param ValidatorState $state
341
     * @param Certificate    $cert
342
     */
343 35
    private function _checkPermittedSubtrees(ValidatorState $state, Certificate $cert)
344
    {
345
        // @todo Implement
346 35
        $state->permittedSubtrees();
347 35
    }
348
349
    /**
350
     * @param ValidatorState $state
351
     * @param Certificate    $cert
352
     */
353 35
    private function _checkExcludedSubtrees(ValidatorState $state, Certificate $cert)
354
    {
355
        // @todo Implement
356 35
        $state->excludedSubtrees();
357 35
    }
358
359
    /**
360
     * Apply policy mappings handling for the preparation step.
361
     *
362
     * @param ValidatorState $state
363
     * @param Certificate    $cert
364
     *
365
     * @throws PathValidationException
366
     *
367
     * @return ValidatorState
368
     */
369 43
    private function _preparePolicyMappings(ValidatorState $state,
370
        Certificate $cert): ValidatorState
371
    {
372 43
        $extensions = $cert->tbsCertificate()->extensions();
373 43
        if ($extensions->hasPolicyMappings()) {
374
            // (a) verify that anyPolicy mapping is not used
375 4
            if ($extensions->policyMappings()->hasAnyPolicyMapping()) {
376 1
                throw new PathValidationException('anyPolicy mapping found.');
377
            }
378
            // (b) process policy mappings
379 3
            if ($state->hasValidPolicyTree()) {
380 3
                $state = $state->validPolicyTree()->processMappings($state, $cert);
381
            }
382
        }
383 42
        return $state;
384
    }
385
386
    /**
387
     * Apply name constraints handling for the preparation step.
388
     *
389
     * @param ValidatorState $state
390
     * @param Certificate    $cert
391
     *
392
     * @return ValidatorState
393
     */
394 42
    private function _prepareNameConstraints(ValidatorState $state,
395
        Certificate $cert): ValidatorState
396
    {
397 42
        $extensions = $cert->tbsCertificate()->extensions();
398 42
        if ($extensions->hasNameConstraints()) {
399 1
            $state = $this->_processNameConstraints($state, $cert);
400
        }
401 42
        return $state;
402
    }
403
404
    /**
405
     * Apply preparation for a non-self-signed certificate.
406
     *
407
     * @param ValidatorState $state
408
     *
409
     * @return ValidatorState
410
     */
411 21
    private function _prepareNonSelfIssued(ValidatorState $state): ValidatorState
412
    {
413
        // (h.1)
414 21
        if ($state->explicitPolicy() > 0) {
415 20
            $state = $state->withExplicitPolicy($state->explicitPolicy() - 1);
416
        }
417
        // (h.2)
418 21
        if ($state->policyMapping() > 0) {
419 20
            $state = $state->withPolicyMapping($state->policyMapping() - 1);
420
        }
421
        // (h.3)
422 21
        if ($state->inhibitAnyPolicy() > 0) {
423 21
            $state = $state->withInhibitAnyPolicy($state->inhibitAnyPolicy() - 1);
424
        }
425 21
        return $state;
426
    }
427
428
    /**
429
     * Apply policy constraints handling for the preparation step.
430
     *
431
     * @param ValidatorState $state
432
     * @param Certificate    $cert
433
     *
434
     * @return ValidatorState
435
     */
436 42
    private function _preparePolicyConstraints(ValidatorState $state,
437
        Certificate $cert): ValidatorState
438
    {
439 42
        $extensions = $cert->tbsCertificate()->extensions();
440 42
        if (!$extensions->hasPolicyConstraints()) {
441 41
            return $state;
442
        }
443 2
        $ext = $extensions->policyConstraints();
444
        // (i.1)
445 2
        if ($ext->hasRequireExplicitPolicy() &&
446 2
                $ext->requireExplicitPolicy() < $state->explicitPolicy()) {
447 2
            $state = $state->withExplicitPolicy($ext->requireExplicitPolicy());
448
        }
449
        // (i.2)
450 2
        if ($ext->hasInhibitPolicyMapping() &&
451 2
                $ext->inhibitPolicyMapping() < $state->policyMapping()) {
452 1
            $state = $state->withPolicyMapping($ext->inhibitPolicyMapping());
453
        }
454 2
        return $state;
455
    }
456
457
    /**
458
     * Apply inhibit any-policy handling for the preparation step.
459
     *
460
     * @param ValidatorState $state
461
     * @param Certificate    $cert
462
     *
463
     * @return ValidatorState
464
     */
465 42
    private function _prepareInhibitAnyPolicy(ValidatorState $state,
466
        Certificate $cert): ValidatorState
467
    {
468 42
        $extensions = $cert->tbsCertificate()->extensions();
469 42
        if ($extensions->hasInhibitAnyPolicy()) {
470 2
            $ext = $extensions->inhibitAnyPolicy();
471 2
            if ($ext->skipCerts() < $state->inhibitAnyPolicy()) {
472 2
                $state = $state->withInhibitAnyPolicy($ext->skipCerts());
473
            }
474
        }
475 42
        return $state;
476
    }
477
478
    /**
479
     * Verify maximum certification path length for the preparation step.
480
     *
481
     * @param ValidatorState $state
482
     * @param Certificate    $cert
483
     *
484
     * @throws PathValidationException
485
     *
486
     * @return ValidatorState
487
     */
488 40
    private function _verifyMaxPathLength(ValidatorState $state,
489
        Certificate $cert): ValidatorState
490
    {
491 40
        if (!$cert->isSelfIssued()) {
492 21
            if ($state->maxPathLength() <= 0) {
493 2
                throw new PathValidationException(
494 2
                    'Certification path length exceeded.');
495
            }
496 19
            $state = $state->withMaxPathLength($state->maxPathLength() - 1);
497
        }
498 40
        return $state;
499
    }
500
501
    /**
502
     * Check key usage extension for the preparation step.
503
     *
504
     * @param Certificate $cert
505
     *
506
     * @throws PathValidationException
507
     */
508 40
    private function _checkKeyUsage(Certificate $cert): void
509
    {
510 40
        $extensions = $cert->tbsCertificate()->extensions();
511 40
        if ($extensions->hasKeyUsage()) {
512 20
            $ext = $extensions->keyUsage();
513 20
            if (!$ext->isKeyCertSign()) {
514 1
                throw new PathValidationException('keyCertSign usage not set.');
515
            }
516
        }
517 39
    }
518
519
    /**
520
     * @param ValidatorState $state
521
     * @param Certificate    $cert
522
     *
523
     * @return ValidatorState
524
     */
525 1
    private function _processNameConstraints(ValidatorState $state,
526
        Certificate $cert): ValidatorState
527
    {
528
        // @todo Implement
529 1
        return $state;
530
    }
531
532
    /**
533
     * Process basic constraints extension.
534
     *
535
     * @param Certificate $cert
536
     *
537
     * @throws PathValidationException
538
     */
539 42
    private function _processBasicContraints(Certificate $cert): void
540
    {
541 42
        if (TBSCertificate::VERSION_3 === $cert->tbsCertificate()->version()) {
542 39
            $extensions = $cert->tbsCertificate()->extensions();
543 39
            if (!$extensions->hasBasicConstraints()) {
544 1
                throw new PathValidationException(
545 1
                    'v3 certificate must have basicConstraints extension.');
546
            }
547
            // verify that cA is set to TRUE
548 38
            if (!$extensions->basicConstraints()->isCA()) {
549 1
                throw new PathValidationException(
550 1
                    'Certificate is not a CA certificate.');
551
            }
552
        }
553 40
    }
554
555
    /**
556
     * Process pathLenConstraint.
557
     *
558
     * @param ValidatorState $state
559
     * @param Certificate    $cert
560
     *
561
     * @return ValidatorState
562
     */
563 40
    private function _processPathLengthContraint(ValidatorState $state,
564
        Certificate $cert): ValidatorState
565
    {
566 40
        $extensions = $cert->tbsCertificate()->extensions();
567 40
        if ($extensions->hasBasicConstraints()) {
568 37
            $ext = $extensions->basicConstraints();
569 37
            if ($ext->hasPathLen()) {
570 26
                if ($ext->pathLen() < $state->maxPathLength()) {
571 9
                    $state = $state->withMaxPathLength($ext->pathLen());
572
                }
573
            }
574
        }
575 40
        return $state;
576
    }
577
578
    /**
579
     * @param ValidatorState $state
580
     * @param Certificate    $cert
581
     *
582
     * @return ValidatorState
583
     */
584 39
    private function _processExtensions(ValidatorState $state,
585
        Certificate $cert): ValidatorState
586
    {
587
        // @todo Implement
588 39
        return $state;
589
    }
590
591
    /**
592
     * @param ValidatorState $state
593
     *
594
     * @return ValidatorState
595
     */
596 30
    private function _calculatePolicyIntersection(ValidatorState $state): ValidatorState
597
    {
598
        // (i) If the valid_policy_tree is NULL, the intersection is NULL
599 30
        if (!$state->hasValidPolicyTree()) {
600 21
            return $state;
601
        }
602
        // (ii) If the valid_policy_tree is not NULL and
603
        // the user-initial-policy-set is any-policy, the intersection
604
        // is the entire valid_policy_tree
605 9
        $initial_policies = $this->_config->policySet();
606 9
        if (in_array(PolicyInformation::OID_ANY_POLICY, $initial_policies)) {
607 3
            return $state;
608
        }
609
        // (iii) If the valid_policy_tree is not NULL and the
610
        // user-initial-policy-set is not any-policy, calculate
611
        // the intersection of the valid_policy_tree and the
612
        // user-initial-policy-set as follows
613 6
        return $state->validPolicyTree()->calculateIntersection($state, $initial_policies);
614
    }
615
}
616