Completed
Push — master ( 2c97ea...cf2c3c )
by Florent
02:00
created

RegistrationResponse::extractKeyData()   A

Complexity

Conditions 4
Paths 15

Size

Total Lines 51
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 37
nc 15
nop 1
dl 0
loc 51
rs 9.328
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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