Completed
Push — master ( 698a0f...8180db )
by Florent
01:57
created

checkCertificate()   A

Complexity

Conditions 6
Paths 1

Size

Total Lines 18
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 6
nc 1
nop 2
dl 0
loc 18
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * The MIT License (MIT)
7
 *
8
 * Copyright (c) 2014-2018 Spomky-Labs
9
 *
10
 * This software may be modified and distributed under the terms
11
 * of the MIT license.  See the LICENSE file for details.
12
 */
13
14
namespace U2FAuthentication\Fido2\AttestationStatement;
15
16
use Assert\Assertion;
17
use U2FAuthentication\Fido2\AuthenticatorData;
18
19
final class PackedAttestationStatementSupport implements AttestationStatementSupport
20
{
21
    private const CERTIFICATES_HASHES = [
22
        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
23
        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
24
        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
25
        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
26
        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
27
        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
28
    ];
29
30
    public function name(): string
31
    {
32
        return 'packed';
33
    }
34
35
    public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
36
    {
37
        Assertion::true($attestationStatement->has('sig'), 'The attestation statement value "sig" is missing.');
38
39
        switch (true) {
40
            case $attestationStatement->has('x5c'):
41
                return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData);
42
            case $attestationStatement->has('ecdaaKeyId'):
43
                return $this->processWithECDAA();
44
            default:
45
                return $this->processWithSelfAttestation();
46
        }
47
    }
48
49
    private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
50
    {
51
        $certificates = $attestationStatement->get('x5c');
52
        Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
53
        Assertion::notEmpty($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
54
55
        //Check certificate CA chain and returns the Attestation Certificate
56
        $attestnCert = $this->loadFromX5C($certificates);
57
58
        $signedData = $authenticatorData->getAuthData().$clientDataJSONHash;
59
        $result = openssl_verify($signedData, $attestationStatement->get('sig'), $attestnCert, OPENSSL_ALGO_SHA256);
60
        if (1 !== $result) {
61
            return false;
62
        }
63
64
        $this->checkCertificate($attestnCert, $authenticatorData);
65
66
        return true;
67
    }
68
69
    private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
70
    {
71
        $parsed = openssl_x509_parse($attestnCert);
72
73
        //Check version
74
        Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
75
76
        //Check subject field
77
        Assertion::false(!isset($parsed['name']) || false === mb_strpos($parsed['name'], '/OU=Authenticator Attestation'), 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"');
78
79
        //Check extensions
80
        Assertion::false(!isset($parsed['extensions']) || !\is_array($parsed['extensions']), 'Certificate extensions are missing');
81
82
        //Check certificate is not a CA cert
83
        Assertion::false(!isset($parsed['extensions']['basicConstraints']) || 'CA:FALSE' !== $parsed['extensions']['basicConstraints'], 'The Basic Constraints extension must have the CA component set to false');
84
85
        // id-fido-gen-ce-aaguid OID check
86
        Assertion::false(\in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($authenticatorData->getAttestedCredentialData()->getAaguid(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate');
87
    }
88
89
    private function processWithECDAA(): bool
90
    {
91
        throw new \RuntimeException('ECDAA not supported');
92
    }
93
94
    private function processWithSelfAttestation(): bool
95
    {
96
        throw new \RuntimeException('Self attestation not supported');
97
    }
98
99
    private function getX509Certificate(string $publicKey): string
100
    {
101
        $derCertificate = $this->unusedBytesFix($publicKey);
102
        $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
103
        $pemCert .= chunk_split(base64_encode($derCertificate), 64, PHP_EOL);
104
        $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL;
105
106
        return $pemCert;
107
    }
108
109
    private function unusedBytesFix(string $derCertificate): string
110
    {
111
        $certificateHash = hash('sha256', $derCertificate);
112
        if (\in_array($certificateHash, self::CERTIFICATES_HASHES, true)) {
113
            $derCertificate[mb_strlen($derCertificate, '8bit') - 257] = "\0";
114
        }
115
116
        return $derCertificate;
117
    }
118
119
    private function loadFromX5C(array $x5c): string
120
    {
121
        $certificate = null;
122
        $last_issuer = null;
123
        $last_subject = null;
124
        foreach ($x5c as $cert) {
125
            $current_cert = $this->getX509Certificate($cert);
126
            $x509 = \Safe\openssl_x509_read($current_cert);
127
            if (false === $x509) {
128
                $last_issuer = null;
129
                $last_subject = null;
130
131
                break;
132
            }
133
            $parsed = \openssl_x509_parse($x509);
134
135
            \openssl_x509_free($x509);
136
            if (false === $parsed) {
137
                $last_issuer = null;
138
                $last_subject = null;
139
140
                break;
141
            }
142
            if (null === $last_subject) {
143
                $last_subject = $parsed['subject'];
144
                $last_issuer = $parsed['issuer'];
145
                $certificate = $current_cert;
146
            } else {
147
                if (\Safe\json_encode($last_issuer) === \Safe\json_encode($parsed['subject'])) {
148
                    $last_subject = $parsed['subject'];
149
                    $last_issuer = $parsed['issuer'];
150
                } else {
151
                    $last_issuer = null;
152
                    $last_subject = null;
153
154
                    break;
155
                }
156
            }
157
        }
158
159
        switch (true) {
160
            case null !== $certificate && 1 === \count($x5c):
161
                return $certificate;
162
            case null === $certificate:
163
            case \Safe\json_encode($last_issuer) !== \Safe\json_encode($last_subject):
164
                throw new \InvalidArgumentException('Invalid certificate chain.');
165
            default:
166
                return $certificate;
167
        }
168
    }
169
}
170