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.
Test Failed
Push — master ( 405cf3...79c9ba )
by Joni
04:48
created

PathValidator::_verifySignature()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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