Completed
Push — master ( 63a2ed...6e6341 )
by Paul
03:41
created

U2F.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 *
4
 * @copyright (c) 2015, Paul Sohier
5
 * @copyright (c) 2014 Yubico AB
6
 * @license BSD-2-Clause
7
 *
8
 *
9
 * Orignal Copyright:
10
 * Copyright (c) 2014 Yubico AB
11
 * All rights reserved.
12
 *
13
 * Redistribution and use in source and binary forms, with or without
14
 * modification, are permitted provided that the following conditions are
15
 * met:
16
 *
17
 *   * Redistributions of source code must retain the above copyright
18
 *     notice, this list of conditions and the following disclaimer.
19
 *
20
 *   * Redistributions in binary form must reproduce the above
21
 *     copyright notice, this list of conditions and the following
22
 *     disclaimer in the documentation and/or other materials provided
23
 *     with the distribution.
24
 *
25
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
31
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
33
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
 */
37
38
namespace paul999\u2f;
39
40
/** Constant for the version of the u2f protocol */
41
use paul999\u2f\Exceptions\U2fError;
42
43
44
class U2F implements U2F_interface
45
{
46
    /** @var string */
47
    private $appId;
48
49
    /** @var null|string */
50
    private $attestDir;
51
52
    /** @internal */
53
    private $FIXCERTS = array(
54
        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
55
        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
56
        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
57
        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
58
        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
59
        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
60
    );
61
62
    /**
63
     * @param string $appId Application id for the running application
64
     * @param string|null $attestDir Directory where trusted attestation roots may be found
65
     * @throws U2fError If OpenSSL older than 1.0.0 is used
66
     */
67
    public function __construct($appId, $attestDir = null)
68
    {
69
        if (OPENSSL_VERSION_NUMBER < 0x10000000) {
70
            throw new U2fError('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, U2fError::ERR_OLD_OPENSSL);
71
        }
72
        $this->appId = $appId;
73
        $this->attestDir = $attestDir;
74
    }
75
76
    public function getRegisterData(array $registrations = array())
77
    {
78
        $challenge = $this->createChallenge();
79
        $request = new RegisterRequest($challenge, $this->appId);
80
        $signs = $this->getAuthenticateData($registrations);
81
        return array($request, $signs);
82
    }
83
84
    public function doRegister(RegisterRequestInterface $request, RegisterResponseInterface $response, $includeCert = true)
85
    {
86
        if ($response->getErrorCode() != null) {
87
            throw new U2fError('User-agent returned error. Error code: ' . $response->getErrorCode(), U2fError::ERR_BAD_UA_RETURNING);
88
        }
89
90
        if (!is_bool($includeCert)) {
91
            throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
92
        }
93
94
        $rawReg = $this->base64u_decode($response->getRegistrationData());
95
        $regData = array_values(unpack('C*', $rawReg));
96
        $clientData = $this->base64u_decode($response->getClientData());
97
        $cli = json_decode($clientData);
98
99
        if ($cli->challenge !== $request->getChallenge()) {
100
            throw new U2fError('Registration challenge does not match', U2fError::ERR_UNMATCHED_CHALLENGE);
101
        }
102
103
        $registration = new Registration();
104
        $offs = 1;
105
        $pubKey = substr($rawReg, $offs, self::PUBKEY_LEN);
106
        $offs += self::PUBKEY_LEN;
107
        // decode the pubKey to make sure it's good
108
        $tmpKey = $this->pubkey_to_pem($pubKey);
109
        if ($tmpKey === null) {
110
            throw new U2fError('Decoding of public key failed', U2fError::ERR_PUBKEY_DECODE);
111
        }
112
        $registration->setPublicKey(base64_encode($pubKey));
113
        $khLen = $regData[$offs++];
114
        $kh = substr($rawReg, $offs, $khLen);
115
        $offs += $khLen;
116
        $registration->setKeyHandle($this->base64u_encode($kh));
117
118
        // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
119
        $certLen = 4;
120
        $certLen += ($regData[$offs + 2] << 8);
121
        $certLen += $regData[$offs + 3];
122
123
        $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
124
        $offs += $certLen;
125
        $pemCert = "-----BEGIN CERTIFICATE-----\r\n";
126
        $pemCert .= chunk_split(base64_encode($rawCert), 64);
127
        $pemCert .= "-----END CERTIFICATE-----";
128
        if ($includeCert) {
129
            $registration->setCertificate(base64_encode($rawCert));
130
        }
131
        if ($this->attestDir !== null) {
132
            if (openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
133
                throw new U2fError('Attestation certificate can not be validated', U2fError::ERR_ATTESTATION_VERIFICATION);
134
            }
135
        }
136
137
        if (!openssl_pkey_get_public($pemCert)) {
138
            throw new U2fError('Decoding of public key failed', U2fError::ERR_PUBKEY_DECODE);
139
        }
140
        $signature = substr($rawReg, $offs);
141
142
        $dataToVerify = chr(0);
143
        $dataToVerify .= hash('sha256', $request->getAppId(), true);
144
        $dataToVerify .= hash('sha256', $clientData, true);
145
        $dataToVerify .= $kh;
146
        $dataToVerify .= $pubKey;
147
148
        if (openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
149
            return $registration;
150
        } else {
151
            throw new U2fError('Attestation signature does not match', U2fError::ERR_ATTESTATION_SIGNATURE);
152
        }
153
    }
154
155
    public function getAuthenticateData(array $registrations)
156
    {
157
        $sigs = array();
158
        foreach ($registrations as $reg) {
159
            if (!($reg instanceof RegistrationInterface)) {
160
                throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
161
            }
162
            $sigs[] = new SignRequest($this->createChallenge(), $reg->getKeyHandle(), $this->appId);
163
        }
164
        return $sigs;
165
    }
166
167
    public function doAuthenticate(array $requests, array $registrations, AuthenticationResponseInterface $response)
168
    {
169
        if ($response->getErrorCode() != null) {
170
            throw new U2fError('User-agent returned error. Error code: ' . $response->getErrorCode(), U2fError::ERR_BAD_UA_RETURNING);
171
        }
172
173
        $clientData = $this->base64u_decode($response->getClientData());
174
        $decodedClient = json_decode($clientData);
175
        /**
176
         * @var SignRequestInterface $req
177
         */
178
        foreach ($requests as $row) {
179
            if (!($row instanceof SignRequestInterface)) {
180
                throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of SignRequest.');
181
            }
182
183
            if ($row->keyHandle === $response->getKeyHandle() && $row->challenge === $decodedClient->challenge) {
0 ignored issues
show
Accessing keyHandle on the interface paul999\u2f\SignRequestInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Accessing challenge on the interface paul999\u2f\SignRequestInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
184
                $req = $row;
185
                break;
186
            }
187
        }
188
        if (!isset($req)) {
189
            throw new U2fError('No matching request found', U2fError::ERR_NO_MATCHING_REQUEST);
190
        }
191
        /**
192
         * @var RegistrationInterface reg
193
         */
194
        foreach ($registrations as $row) {
195
            if (!($row instanceof RegistrationInterface)) {
196
                throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of Registration.');
197
            }
198
199
            if ($row->getKeyHandle() === $response->getKeyHandle()) {
200
                $reg = $row;
201
                break;
202
            }
203
        }
204
        if (!isset($reg)) {
205
            throw new U2fError('No matching registration found', U2fError::ERR_NO_MATCHING_REGISTRATION);
206
        }
207
        $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->getPublicKey()));
208
        if ($pemKey === null) {
209
            throw new U2fError('Decoding of public key failed', U2fError::ERR_PUBKEY_DECODE);
210
        }
211
212
        $signData = $this->base64u_decode($response->getSignatureData());
213
        $dataToVerify = hash('sha256', $req->getAppId(), true);
214
        $dataToVerify .= substr($signData, 0, 5);
215
        $dataToVerify .= hash('sha256', $clientData, true);
216
        $signature = substr($signData, 5);
217
218
        if (openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
219
            $ctr = unpack("Nctr", substr($signData, 1, 4));
220
            $counter = $ctr['ctr'];
221
            /* TODO: wrap-around should be handled somehow.. */
222
            if ($counter > $reg->getCounter()) {
223
                $reg->setCounter($counter);
224
                return $reg;
225
            } else {
226
                throw new U2fError('Counter too low.', U2fError::ERR_COUNTER_TOO_LOW);
227
            }
228
        } else {
229
            throw new U2fError('Authentication failed', U2fError::ERR_AUTHENTICATION_FAILURE);
230
        }
231
    }
232
233
    /**
234
     * @return array
235
     */
236
    private function get_certs()
237
    {
238
        $files = array();
239
        if ($this->attestDir !== null && $handle = opendir($this->attestDir)) {
240
            while (false !== ($entry = @readdir($handle))) {
241
                if (is_file("{$this->attestDir}/$entry")) {
242
                    $files[] = "{$this->attestDir}/$entry";
243
                }
244
            }
245
            closedir($handle);
246
        }
247
        return $files;
248
    }
249
250
    /**
251
     * @param string $data
252
     * @return string
253
     */
254
    private function base64u_encode($data)
255
    {
256
        return trim(strtr(base64_encode($data), '+/', '-_'), '=');
257
    }
258
259
    /**
260
     * @param string $data
261
     * @return string
262
     */
263
    private function base64u_decode($data)
264
    {
265
        return base64_decode(strtr($data, '-_', '+/'));
266
    }
267
268
    /**
269
     * @param string $key
270
     * @return null|string
271
     */
272
    private function pubkey_to_pem($key)
273
    {
274
        if (strlen($key) !== self::PUBKEY_LEN || $key[0] !== "\x04") {
275
            return null;
276
        }
277
278
        /*
279
         * Convert the public key to binary DER format first
280
         * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
281
         *
282
         *  SEQUENCE(2 elem)                        30 59
283
         *   SEQUENCE(2 elem)                       30 13
284
         *    OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
285
         *    OID1.2.840.10045.3.1.7 (secp256r1)    06 08 2a 86 48 ce 3d 03 01 07
286
         *   BIT STRING(520 bit)                    03 42 ..key..
287
         */
288
        $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
289
        $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
290
        $der .= "\0" . $key;
291
292
        $pem = "-----BEGIN PUBLIC KEY-----\r\n";
293
        $pem .= chunk_split(base64_encode($der), 64);
294
        $pem .= "-----END PUBLIC KEY-----";
295
296
        return $pem;
297
    }
298
299
    /**
300
     * @return string
301
     * @throws U2fError
302
     */
303
    private function createChallenge()
304
    {
305
        $challenge = openssl_random_pseudo_bytes(32, $crypto_strong);
306
        if ($crypto_strong !== true) {
307
            throw new U2fError('Unable to obtain a good source of randomness', U2fError::ERR_BAD_RANDOM);
308
        }
309
310
        $challenge = $this->base64u_encode($challenge);
311
312
        return $challenge;
313
    }
314
315
    /**
316
     * Fixes a certificate where the signature contains unused bits.
317
     *
318
     * @param string $cert
319
     * @return mixed
320
     */
321
    private function fixSignatureUnusedBits($cert)
322
    {
323
        if (in_array(hash('sha256', $cert), $this->FIXCERTS)) {
324
            $cert[strlen($cert) - 257] = "\0";
325
        }
326
        return $cert;
327
    }
328
}
329