Completed
Push — master ( 7acc61...cd9e85 )
by Florent
22s
created

RegistrationResponse::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
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\Fido;
15
16
use Base64Url\Base64Url;
17
18
class RegistrationResponse
19
{
20
    private const SUPPORTED_PROTOCOL_VERSIONS = ['U2F_V2'];
21
    private const PUBLIC_KEY_LENGTH = 65;
22
    private const CERTIFICATES_HASHES = [
23
        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
24
        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
25
        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
26
        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
27
        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
28
        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
29
    ];
30
31
    /**
32
     * @var ClientData
33
     */
34
    private $clientData;
35
36
    /**
37
     * @var RegisteredKey
38
     */
39
    private $registeredKey;
40
41
    /**
42
     * @var string
43
     */
44
    private $signature;
45
46
    public function __construct(array $data)
47
    {
48
        if (array_key_exists('errorCode', $data) && 0 !== $data['errorCode']) {
49
            throw new \InvalidArgumentException('Invalid response.');
50
        }
51
52
        $this->checkVersion($data);
53
        $clientData = $this->retrieveClientData($data);
54
        if ('navigator.id.finishEnrollment' !== $clientData->getType()) {
55
            throw new \InvalidArgumentException('Invalid response.');
56
        }
57
        list($publicKey, $keyHandle, $pemCert, $signature) = $this->extractKeyData($data);
58
59
        $this->clientData = $clientData;
60
        $this->registeredKey = new RegisteredKey($data['version'], $keyHandle, $publicKey, $pemCert);
61
        $this->signature = $signature;
62
    }
63
64
    public function getClientData(): ClientData
65
    {
66
        return $this->clientData;
67
    }
68
69
    public function getRegisteredKey(): RegisteredKey
70
    {
71
        return $this->registeredKey;
72
    }
73
74
    public function getSignature(): string
75
    {
76
        return $this->signature;
77
    }
78
79
    /**
80
     * @throws \InvalidArgumentException
81
     */
82
    private function retrieveClientData(array $data): ClientData
83
    {
84
        if (!array_key_exists('clientData', $data) || !\is_string($data['clientData'])) {
85
            throw new \InvalidArgumentException('Invalid response.');
86
        }
87
88
        return new ClientData($data['clientData']);
89
    }
90
91
    /**
92
     * @throws \InvalidArgumentException
93
     */
94
    private function checkVersion(array $data): void
95
    {
96
        if (!array_key_exists('version', $data) || !\is_string($data['version'])) {
97
            throw new \InvalidArgumentException('Invalid response.');
98
        }
99
        if (!\in_array($data['version'], self::SUPPORTED_PROTOCOL_VERSIONS, true)) {
100
            throw new \InvalidArgumentException('Unsupported protocol version.');
101
        }
102
    }
103
104
    /**
105
     * @throws \InvalidArgumentException
106
     */
107
    private function extractKeyData(array $data): array
108
    {
109
        if (!array_key_exists('registrationData', $data) || !\is_string($data['registrationData'])) {
110
            throw new \InvalidArgumentException('Invalid response.');
111
        }
112
        $stream = fopen('php://memory', 'r+');
113
        if (false === $stream) {
114
            throw new \InvalidArgumentException('Unable to load the registration data.');
115
        }
116
        $registrationData = Base64Url::decode($data['registrationData']);
117
        fwrite($stream, $registrationData);
118
        rewind($stream);
119
120
        $reservedByte = fread($stream, 1);
121
        if ("\x05" !== $reservedByte) { // 1 byte reserved with value x05
122
            fclose($stream);
123
124
            throw new \InvalidArgumentException('Bad reserved byte.');
125
        }
126
127
        $publicKey = fread($stream, self::PUBLIC_KEY_LENGTH); // 65 bytes for the public key
128
        if (self::PUBLIC_KEY_LENGTH !== mb_strlen($publicKey, '8bit')) {
129
            fclose($stream);
130
131
            throw new \InvalidArgumentException('Bad public key length.');
132
        }
133
134
        $keyHandleLength = fread($stream, 1); // 1 byte for the key handle length
135
        if (0 === \ord($keyHandleLength)) {
136
            fclose($stream);
137
138
            throw new \InvalidArgumentException('Bad key handle length.');
139
        }
140
141
        $keyHandle = fread($stream, \ord($keyHandleLength)); // x bytes for the key handle
142
        if (mb_strlen($keyHandle, '8bit') !== \ord($keyHandleLength)) {
143
            fclose($stream);
144
145
            throw new \InvalidArgumentException('Bad key handle.');
146
        }
147
148
        $certHeader = fread($stream, 4); // 4 bytes for the certificate header
149
        if (4 !== mb_strlen($certHeader, '8bit')) {
150
            fclose($stream);
151
152
            throw new \InvalidArgumentException('Bad certificate header.');
153
        }
154
155
        $highOrder = \ord($certHeader[2]) << 8;
156
        $lowOrder = \ord($certHeader[3]);
157
        $certLength = $highOrder + $lowOrder;
158
        $certBody = fread($stream, $certLength); // x bytes for the certificate
159
        if (mb_strlen($certBody, '8bit') !== $certLength) {
160
            fclose($stream);
161
162
            throw new \InvalidArgumentException('Bad certificate.');
163
        }
164
        $derCertificate = $this->unusedBytesFix($certHeader.$certBody);
165
        $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
166
        $pemCert .= chunk_split(base64_encode($derCertificate), 64, PHP_EOL);
167
        $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL;
168
169
        $signature = ''; // The rest is the signature
170
        while (!feof($stream)) {
171
            $signature .= fread($stream, 1024);
172
        }
173
        fclose($stream);
174
175
        return [
176
            new PublicKey($publicKey),
177
            new KeyHandler($keyHandle),
178
            $pemCert,
179
            $signature,
180
        ];
181
    }
182
183
    private function unusedBytesFix(string $derCertificate): string
184
    {
185
        $certificateHash = hash('sha256', $derCertificate);
186
        if (\in_array($certificateHash, self::CERTIFICATES_HASHES, true)) {
187
            $derCertificate[mb_strlen($derCertificate, '8bit') - 257] = "\0";
188
        }
189
190
        return $derCertificate;
191
    }
192
193
    /**
194
     * @param string[] $attestationCertificates
195
     */
196
    public function isValid(RegistrationRequest $challenge, array $attestationCertificates = []): bool
197
    {
198
        if (!hash_equals($challenge->getChallenge(), $this->clientData->getChallenge())) {
199
            return false;
200
        }
201
        if (!hash_equals($challenge->getApplicationId(), $this->clientData->getOrigin())) {
202
            return false;
203
        }
204
205
        if (!empty($attestationCertificates) && true !== openssl_x509_checkpurpose($this->registeredKey->getAttestationCertificate(), X509_PURPOSE_ANY, $attestationCertificates)) {
206
            return false;
207
        }
208
209
        $dataToVerify = "\0";
210
        $dataToVerify .= hash('sha256', $this->clientData->getOrigin(), true);
211
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
212
        $dataToVerify .= $this->registeredKey->getKeyHandler();
213
        $dataToVerify .= $this->registeredKey->getPublicKey();
214
215
        return 1 === openssl_verify($dataToVerify, $this->signature, $this->registeredKey->getAttestationCertificate(), OPENSSL_ALGO_SHA256);
216
    }
217
}
218