Failed Conditions
Push — master ( bbfade...32fb37 )
by Florent
02:44
created

RegistrationResponse::unusedBytesFix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
rs 9.4285
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) 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
    /**
47
     * RegistrationChallengeMiddleware constructor.
48
     *
49
     * @param array $data
50
     */
51
    private function __construct(array $data)
52
    {
53
        if (array_key_exists('errorCode', $data) && 0 !== $data['errorCode']) {
54
            throw new \InvalidArgumentException('Invalid response.');
55
        }
56
57
        $this->checkVersion($data);
58
        $clientData = $this->retrieveClientData($data);
59
        if ('navigator.id.finishEnrollment' !== $clientData->getType()) {
60
            throw new \InvalidArgumentException('Invalid response.');
61
        }
62
        list($publicKey, $keyHandle, $pemCert, $signature) = $this->extractKeyData($data);
63
64
        $this->clientData = $clientData;
65
        $this->registeredKey = RegisteredKey::create($data['version'], $keyHandle, $publicKey, $pemCert);
66
        $this->signature = $signature;
67
    }
68
69
    /**
70
     * @param array $data
71
     *
72
     * @return RegistrationResponse
73
     */
74
    public static function create(array $data): self
75
    {
76
        return new self($data);
77
    }
78
79
    /**
80
     * @return ClientData
81
     */
82
    public function getClientData(): ClientData
83
    {
84
        return $this->clientData;
85
    }
86
87
    /**
88
     * @return RegisteredKey
89
     */
90
    public function getRegisteredKey(): RegisteredKey
91
    {
92
        return $this->registeredKey;
93
    }
94
95
    /**
96
     * @return string
97
     */
98
    public function getSignature(): string
99
    {
100
        return $this->signature;
101
    }
102
103
    /**
104
     * @param array $data
105
     *
106
     * @throws \InvalidArgumentException
107
     *
108
     * @return ClientData
109
     */
110
    private function retrieveClientData(array $data): ClientData
111
    {
112
        if (!array_key_exists('clientData', $data) || !is_string($data['clientData'])) {
113
            throw new \InvalidArgumentException('Invalid response.');
114
        }
115
116
        return ClientData::create($data['clientData']);
117
    }
118
119
    /**
120
     * @param array $data
121
     *
122
     * @throws \InvalidArgumentException
123
     */
124
    private function checkVersion(array $data): void
125
    {
126
        if (!array_key_exists('version', $data) || !is_string($data['version'])) {
127
            throw new \InvalidArgumentException('Invalid response.');
128
        }
129
        if (!in_array($data['version'], self::SUPPORTED_PROTOCOL_VERSIONS)) {
130
            throw new \InvalidArgumentException('Unsupported protocol version.');
131
        }
132
    }
133
134
    /**
135
     * @param array $data
136
     *
137
     * @throws \InvalidArgumentException
138
     *
139
     * @return array
140
     */
141
    private function extractKeyData(array $data): array
142
    {
143
        if (!array_key_exists('registrationData', $data) || !is_string($data['registrationData'])) {
144
            throw new \InvalidArgumentException('Invalid response.');
145
        }
146
        $stream = fopen('php://memory', 'r+');
147
        if (false === $stream) {
148
            throw new \InvalidArgumentException('Unable to load the registration data.');
149
        }
150
        $registrationData = Base64Url::decode($data['registrationData']);
151
        fwrite($stream, $registrationData);
152
        rewind($stream);
153
154
        $reservedByte = fread($stream, 1);
155
        if ("\x05" !== $reservedByte) { // 1 byte reserved with value x05
156
            fclose($stream);
157
158
            throw new \InvalidArgumentException('Bad reserved byte.');
159
        }
160
161
        $publicKey = fread($stream, self::PUBLIC_KEY_LENGTH); // 65 bytes for the public key
162
        if (mb_strlen($publicKey, '8bit') !== self::PUBLIC_KEY_LENGTH) {
163
            fclose($stream);
164
165
            throw new \InvalidArgumentException('Bad public key length.');
166
        }
167
168
        $keyHandleLength = fread($stream, 1); // 1 byte for the key handle length
169
        if (ord($keyHandleLength) === 0) {
170
            fclose($stream);
171
172
            throw new \InvalidArgumentException('Bad key handle length.');
173
        }
174
175
        $keyHandle = fread($stream, ord($keyHandleLength)); // x bytes for the key handle
176
        if (mb_strlen($keyHandle, '8bit') !== ord($keyHandleLength)) {
177
            fclose($stream);
178
179
            throw new \InvalidArgumentException('Bad key handle.');
180
        }
181
182
        $certHeader = fread($stream, 4); // 4 bytes for the certificate header
183
        if (mb_strlen($certHeader, '8bit') !== 4) {
184
            fclose($stream);
185
186
            throw new \InvalidArgumentException('Bad certificate header.');
187
        }
188
189
        $highOrder = ord($certHeader[2]) << 8;
190
        $lowOrder = ord($certHeader[3]);
191
        $certLength = $highOrder + $lowOrder;
192
        $certBody = fread($stream, $certLength); // x bytes for the certificate
193
        if (mb_strlen($certBody, '8bit') !== $certLength) {
194
            fclose($stream);
195
196
            throw new \InvalidArgumentException('Bad certificate.');
197
        }
198
        $derCertificate = $this->unusedBytesFix($certHeader.$certBody);
199
        $pemCert = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
200
        $pemCert .= chunk_split(base64_encode($derCertificate), 64, PHP_EOL);
201
        $pemCert .= '-----END CERTIFICATE-----'.PHP_EOL;
202
203
        $signature = ''; // The rest is the signature
204
        while (!feof($stream)) {
205
            $signature .= fread($stream, 1024);
206
        }
207
        fclose($stream);
208
209
        return [
210
            PublicKey::create($publicKey),
211
            KeyHandle::create($keyHandle),
212
            $pemCert,
213
            $signature,
214
        ];
215
    }
216
217
    /**
218
     * @param string $derCertificate
219
     *
220
     * @return string
221
     */
222
    private function unusedBytesFix(string $derCertificate): string
223
    {
224
        $certificateHash = hash('sha256', $derCertificate);
225
        if (in_array($certificateHash, self::CERTIFICATES_HASHES)) {
226
            $derCertificate[mb_strlen($derCertificate, '8bit') - 257] = "\0";
227
        }
228
229
        return $derCertificate;
230
    }
231
232
    /**
233
     * @param RegistrationRequest $challenge
234
     * @param string[]            $attestationCertificates
235
     *
236
     * @return bool
237
     */
238
    public function isValid(RegistrationRequest $challenge, array $attestationCertificates = []): bool
239
    {
240
        if (!hash_equals($challenge->getChallenge(), $this->clientData->getChallenge())) {
241
            return false;
242
        }
243
        if (!hash_equals($challenge->getApplicationId(), $this->clientData->getOrigin())) {
244
            return false;
245
        }
246
247
        if (!empty($attestationCertificates) && openssl_x509_checkpurpose($this->registeredKey->getAttestationCertificate(), X509_PURPOSE_ANY, $attestationCertificates) !== true) {
248
            return false;
249
        }
250
251
        $dataToVerify = "\0";
252
        $dataToVerify .= hash('sha256', $this->clientData->getOrigin(), true);
253
        $dataToVerify .= hash('sha256', $this->clientData->getRawData(), true);
254
        $dataToVerify .= $this->registeredKey->getKeyHandler();
255
        $dataToVerify .= $this->registeredKey->getPublicKey();
256
257
        return openssl_verify($dataToVerify, $this->signature, $this->registeredKey->getAttestationCertificate(), OPENSSL_ALGO_SHA256) === 1;
258
    }
259
}
260