Completed
Push — master ( c08e60...32c17a )
by Florent
04:38 queued 01:52
created

getX509Certificate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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