├── .gitignore ├── LICENCE.md ├── README.md ├── composer.json └── src ├── Registration.php ├── RegistrationRequest.php ├── SignRequest.php ├── U2FException.php └── U2FServer.php /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2016, Samyoul 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # U2F-php-server 2 | 3 | [![Latest Stable Version](https://img.shields.io/packagist/v/samyoul/u2f-php-server.svg?style=flat-square)](https://packagist.org/packages/samyoul/u2f-php-server) 4 | [![License](https://img.shields.io/badge/license-BSD_2_Clause-brightgreen.svg?style=flat-square)](LICENCE.md) 5 | 6 | Server-side handling of FIDO U2F registration and authentication for PHP. 7 | 8 | Securing your online accounts and doing your bit to protect your data is extremely important and increasingly more so as hackers get more sophisticated. 9 | FIDO's U2F enables you to add a simple unobtrusive method of 2nd factor authentication, allowing users of your service and/or application to link a hardware key to their account. 10 | 11 | ## The Family of U2F php Libraries 12 | 13 | **Base Library** 14 | 15 | https://github.com/Samyoul/U2F-php-server 16 | 17 | **Fido Test Suite (UTD)** 18 | 19 | https://github.com/Samyoul/U2F-php-UTD 20 | 21 | **Frameworks** 22 | 23 | *Laravel* https://github.com/Samyoul/U2F-Laravel-server 24 | 25 | *Yii* https://github.com/Samyoul/U2F-Yii-server 26 | 27 | *CodeIgniter* https://github.com/Samyoul/U2F-CodeIgniter-server 28 | 29 | 30 | ## Contents 31 | 32 | 1. [Installation](#installation) 33 | 2. [Requirements](#requirements) 34 | 1. [OpenSSL](#openssl) 35 | 1. [Clientside Magic](#client-side-the-magic-javascript-bit-of-talking-with-a-usb-device) 36 | 1. [HTTPS and SSL](#https-and-ssl) 37 | 3. [Terminology](#terminology) 38 | 4. [Recommended Datastore Structure](#recommended-datastore-structure) 39 | 5. [Process Workflow](#process-workflow) 40 | 1. [Registration Process Flow](#registration-process-flow) 41 | 1. [Authentication Process Flow](#authentication-process-flow) 42 | 6. [Example Code](#example-code) 43 | 1. [Compatibility Check](#compatibility-code) 44 | 1. [Registration Code](#registration-code) 45 | 1. [Authentication Code](#authentication-code) 46 | 7. [Frameworks](#frameworks) 47 | 1. [Laravel](#laravel-framework) 48 | 1. [Yii](#yii-framework) 49 | 1. [CodeIgniter](#codeigniter-framework) 50 | 8. [Licence](#licence) 51 | 9. [Credits](#credits) 52 | 53 | ## Installation 54 | 55 | `composer require samyoul/u2f-php-server` 56 | 57 | ## Requirements 58 | 59 | A few **things you need** to know before working with this: 60 | 61 | 1. [**_OpenSSL_**](#openssl) You need at least OpenSSL 1.0.0 or higher. 62 | 2. [**_Client-side Handling_**](#client-side) You need to be able to communicate with a some kind of device. 63 | 3. [**_A HTTPS URL_**](#https-and-ssl) This is very important, without HTTPS U2F simply will not work. 64 | 4. [**_A Datastore_**](#recommended-datastore-structure) You need some kind of datastore for all your U2F registered users (although if you have a system with user authentication I'm presuming you've got this one sorted). 65 | 66 | ### OpenSSL 67 | 68 | This repository requires OpenSSL 1.0.0 or higher. For further details on installing OpenSSL please refer to the php manual http://php.net/manual/en/openssl.installation.php . 69 | 70 | Also see [Compatibility Code](#compatibility-code), to check if you have the correct version of OpenSSL installed, and are unsure how else to check. 71 | 72 | ### Client-side (The magic JavaScript Bit of talking with a USB device) 73 | 74 | My presumption is that if you are looking to add U2F authentication to a php system, then you'll probably are also looking for some client-side handling. You've got a U2F enabled USB device and you want to get the USB device speaking with the browser and then with your server running php. 75 | 76 | 1. Google already have this bit sorted : https://github.com/google/u2f-ref-code/blob/master/u2f-gae-demo/war/js/u2f-api.js 77 | 2. [Mastahyeti](https://github.com/mastahyeti) has created a repo dedicated to Google's JavaScript Client-side API : https://github.com/mastahyeti/u2f-api 78 | 79 | ### HTTPS and SSL 80 | 81 | For U2F to work your website/service must use a HTTPS URL. Without a HTTPS URL your code won't work, so get one for your localhost, get one for your production. https://letsencrypt.org/ 82 | Basically encrypt everything. 83 | 84 | 85 | ## Terminology 86 | 87 | **_HID_** : _Human Interface Device_, like A USB Device [like these things](https://www.google.co.uk/search?q=fido+usb+key&safe=off&tbm=isch) 88 | 89 | ## Recommended Datastore Structure 90 | 91 | You don't need to follow this structure exactly, but you will need to associate your Registration data with a user. You'll also need to store the key handle, public key and the certificate, counter isn't 100% essential but it makes your application more secure. 92 | 93 | 94 | |Name|Type|Description| 95 | |---|---|---| 96 | |id|integer primary key|| 97 | |user_id|integer|| 98 | |key_handle|varchar(255)|| 99 | |public_key|varchar(255)|| 100 | |certificate|text|| 101 | |counter|integer|| 102 | 103 | TODO the descriptions 104 | 105 | ## Process Workflow 106 | 107 | ### Registration Process Flow 108 | 109 | 1. User navigates to a 2nd factor authentication page in your application. 110 | 111 | ... TODO add the rest of the registration process flow ... 112 | 113 | ### Authentication Process Flow 114 | 115 | 1. User navigates to their login page as they usually would, submits username and password. 116 | 1. Server received POST request authentication data, normal username + password validation occurs 117 | 1. On successful authentication, the application checks 2nd factor authentication is required. We're going to presume it is, otherwise the user would just be logged in at this stage. 118 | 1. Application gets the user's registered signatures from the application datastore: `$registrations`. 119 | 1. Application gets its ID, usually the domain the application is accessible from: `$appId` 120 | 1. Application makes a `U2F::makeAuthentication($registrations, $appId)` call, the method returns an array of `SignRequest` objects: `$authenticationRequest`. 121 | 1. Application JSON encodes the array and passes the data to the view 122 | 1. When the browser loads the page the JavaScript fires the `u2f.sign(authenticationRequest, function(data){ // Callback logic })` function 123 | 1. The view will use JavaScript / Browser to poll the host machine's ports for a FIDO U2F device 124 | 1. Once the HID has been found the JavaScript / Browser will send the sign request with data. 125 | 1. The HID will prompt the user to authorise the sign request 126 | 1. On success the HID returns authentication data 127 | 1. The JavaScript receives the HID's returned data and passes it to the server 128 | 1. The application takes the returned data passes it to the `U2F::authenticate($authenticationRequest, $registrations, $authenticationResponse)` method 129 | 1. If the method returns a registration and doesn't throw an Exception, authentication is complete. 130 | 1. Set the user's session, inform the user of the success, and redirect them. 131 | 132 | ## Example Code 133 | 134 | For a full working code example for this repository please see [the dedicated example repository](https://github.com/Samyoul/U2F-php-server-examples) 135 | 136 | You can also install it with the following: 137 | 138 | ```bash 139 | $ git clone https://github.com/Samyoul/U2F-php-server-examples.git 140 | $ cd u2f-php-server-examples 141 | $ composer install 142 | ``` 143 | 144 | 145 | 1. [Compatibility Code](#compatibility-code) 146 | 2. [Registration Code](#registration-code) 147 | 1. [Step 1: Starting](#registration-step-1) 148 | 1. [Step 2: Talking to the HID](#registration-step-2) 149 | 1. [Step 3: Validation & Storage](#registration-step-3) 150 | 3. [Authentication Code]() 151 | 1. [Step 1: Starting]() 152 | 1. [Step 2: Talking to the HID]() 153 | 1. [Step 3: Validation]() 154 | 155 | --- 156 | 157 | ### Compatibility Code 158 | 159 | You'll only ever need to use this method call once per installation and only in the context of debugging if the class is giving you unexpected errors. This method call will check your OpenSSL version and ensure it is at least 1.0.0 . 160 | 161 | ```php 162 | 211 | 212 | U2F Key Registration 213 | 214 | 215 |

U2F Registration

216 |

Please enter your FIDO U2F device into your computer's USB port. Then confirm registration on the device.

217 | 218 |
219 |
220 | 221 |
222 |
223 | 224 | 225 | 252 | 253 | 254 | ``` 255 | 256 | #### Registration Step 3: 257 | **Validation and Key Storage** 258 | 259 | This is the last stage of registration. Validate the registration response data against the original request data. 260 | 261 | ```php 262 | storeRegistration($validatedRegistration); 283 | 284 | // Then let your user know what happened 285 | $userMessage = "Success"; 286 | } catch( Exception $e ) { 287 | $userMessage = "We had an error: ". $e->getMessage(); 288 | } 289 | 290 | //Fictitious view. 291 | echo View::make('template/location/u2f-registration-result.html', compact('userMessage')); 292 | ``` 293 | 294 | --- 295 | 296 | ### Authentication Code 297 | 298 | #### Authentication Step 1: 299 | **Starting the authentication process:** 300 | 301 | We assume that user has successfully authenticated and has previously registered to use FIDO U2F. 302 | 303 | ```php 304 | U2FRegistrations(); 316 | 317 | // This can be anything, but usually easier if you choose your applications domain and top level domain. 318 | $appId = "yourdomain.tld"; 319 | 320 | // Call the U2F makeAuthentication method, passing in the user's registration(s) and the app ID 321 | $authenticationRequest = U2F::makeAuthentication($registrations, $appId); 322 | 323 | // Store the request for later 324 | $_SESSION['authenticationRequest'] = $authenticationRequest; 325 | 326 | // now pass the data to a fictitious view. 327 | echo View::make('template/location/u2f-authentication.html', compact("authenticationRequest")); 328 | ``` 329 | 330 | #### Authentication Step 2: 331 | **Client-side, Talking To The USB** 332 | 333 | Non-AJAX client-side authentication of U2F key token. AJAX can of course be used in your application, but it is easier to demonstrate a linear process without AJAX and callbacks. 334 | 335 | 336 | ```html 337 | 338 | 339 | U2F Key Authentication 340 | 341 | 342 |

U2F Authentication

343 |

Please enter your FIDO U2F device into your computer's USB port. Then confirm authentication on the device.

344 | 345 |
346 |
347 | 348 |
349 |
350 | 351 | 352 | 379 | 380 | 381 | ``` 382 | 383 | #### Authentication Step 3: 384 | **Validation** 385 | 386 | This is the last stage of authentication. Validate the authentication response data against the original request data. 387 | 388 | ```php 389 | U2FRegistrations(); 401 | 402 | try { 403 | 404 | // Validate the authentication response against the registration request. 405 | // The output are the credentials you need to store for U2F authentication. 406 | $validatedAuthentication = U2F::authenticate( 407 | $_SESSION['authenticationRequest'], 408 | $registrations, 409 | json_decode($_POST['u2f_authentication_response']) 410 | ); 411 | 412 | // Fictitious function representing the updating of the U2F token count integer. 413 | $user->updateU2FRegistrationCount($validatedAuthentication); 414 | 415 | // Then let your user know what happened 416 | $userMessage = "Success"; 417 | } catch( Exception $e ) { 418 | $userMessage = "We had an error: ". $e->getMessage(); 419 | } 420 | 421 | //Fictitious view. 422 | echo View::make('template/location/u2f-authentication-result.html', compact('userMessage')); 423 | ``` 424 | 425 | --- 426 | 427 | Again, if you want to just download some example code to play with just install the full working code examples written for this repository please see [the dedicated example repository](https://github.com/Samyoul/U2F-php-server-examples) 428 | 429 | You can also install it with the following: 430 | 431 | ```bash 432 | $ git clone https://github.com/Samyoul/U2F-php-server-examples.git 433 | $ cd u2f-php-server-examples 434 | $ composer install 435 | ``` 436 | 437 | ## Frameworks 438 | 439 | ### Laravel Framework 440 | 441 | See the dedicated repository : https://github.com/Samyoul/U2F-Laravel-server 442 | 443 | Installation: 444 | 445 | `composer require u2f-laravel-server` 446 | 447 | ### Yii Framework 448 | 449 | See the dedicated repository : https://github.com/Samyoul/U2F-Yii-server 450 | 451 | Installation: 452 | 453 | `composer require u2f-yii-server` 454 | 455 | ### CodeIgniter Framework 456 | 457 | See the dedicated repository : https://github.com/Samyoul/U2F-CodeIgniter-server 458 | 459 | Installation: 460 | 461 | `composer require u2f-codeigniter-server` 462 | 463 | ### Can't see yours? 464 | 465 | **Your favourite php framework not in this list? Get coding and submit a pull request and get your framework extension included here.** 466 | 467 | ## Licence 468 | 469 | The repository is licensed under a BSD license. [Read details here](https://github.com/Samyoul/U2F-php-server/blob/master/LICENCE.md) 470 | 471 | ## Credits 472 | 473 | This repo was originally based on the Yubico php-u2flib-server https://github.com/Yubico/php-u2flib-server 474 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samyoul/u2f-php-server", 3 | "description": "Server side handling class for FIDO U2F registration and authentication", 4 | "license":"BSD-2-Clause", 5 | "authors": [ 6 | { 7 | "name": "Samuel Hawksby-Robinson", 8 | "email": "samuel@samyoul.com" 9 | } 10 | ], 11 | "require": { 12 | "ext-openssl":"*" 13 | }, 14 | "autoload": { 15 | "psr-4": { "Samyoul\\U2F\\U2FServer\\": ["src/"] } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Registration.php: -------------------------------------------------------------------------------- 1 | keyHandle = $keyHandle; 32 | } 33 | 34 | /** 35 | * @param string $publicKey 36 | */ 37 | public function setPublicKey($publicKey) 38 | { 39 | $this->publicKey = $publicKey; 40 | } 41 | 42 | /** 43 | * @param string $certificate 44 | */ 45 | public function setCertificate($certificate) 46 | { 47 | $this->certificate = $certificate; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getKeyHandle() 54 | { 55 | return $this->keyHandle; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getPublicKey() 62 | { 63 | return $this->publicKey; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getCertificate() 70 | { 71 | return $this->certificate; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function getCounter() 78 | { 79 | return $this->counter; 80 | } 81 | } -------------------------------------------------------------------------------- /src/RegistrationRequest.php: -------------------------------------------------------------------------------- 1 | challenge = $challenge; 29 | $this->appId = $appId; 30 | } 31 | 32 | public function version() 33 | { 34 | return $this->version; 35 | } 36 | 37 | public function challenge() 38 | { 39 | return $this->challenge; 40 | } 41 | 42 | public function appId() 43 | { 44 | return $this->appId; 45 | } 46 | 47 | public function jsonSerialize() 48 | { 49 | return [ 50 | 'version' => $this->version, 51 | 'challenge' => $this->challenge, 52 | 'appId' => $this->appId, 53 | ]; 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/SignRequest.php: -------------------------------------------------------------------------------- 1 | challenge = $parameters['challenge']; 29 | $this->keyHandle = $parameters['keyHandle']; 30 | $this->appId = $parameters['appId']; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function version() 37 | { 38 | return $this->version; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function challenge() 45 | { 46 | return $this->challenge; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function keyHandle() 53 | { 54 | return $this->keyHandle; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function appId() 61 | { 62 | return $this->appId; 63 | } 64 | 65 | public function jsonSerialize() 66 | { 67 | return [ 68 | 'version' => $this->version, 69 | 'challenge' => $this->challenge, 70 | 'keyHandle' => $this->keyHandle, 71 | 'appId' => $this->appId, 72 | ]; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/U2FException.php: -------------------------------------------------------------------------------- 1 | $request, 60 | "signatures" => $signatures 61 | ]; 62 | } 63 | 64 | /** 65 | * Called to verify and unpack a registration message. 66 | * 67 | * @param RegistrationRequest $request this is a reply to 68 | * @param object $response response from a user 69 | * @param string $attestDir 70 | * @param bool $includeCert set to true if the attestation certificate should be 71 | * included in the returned Registration object 72 | * @return Registration 73 | * @throws U2FException 74 | */ 75 | public static function register(RegistrationRequest $request, $response, $attestDir = null, $includeCert = true) 76 | { 77 | // Parameter Checks 78 | if( !is_object( $request ) ) { 79 | throw new \InvalidArgumentException('$request of register() method only accepts object.'); 80 | } 81 | 82 | if( !is_object( $response ) ) { 83 | throw new \InvalidArgumentException('$response of register() method only accepts object.'); 84 | } 85 | 86 | if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { 87 | throw new U2FException( 88 | 'User-agent returned error. Error code: ' . $response->errorCode, 89 | U2FException::BAD_UA_RETURNING 90 | ); 91 | } 92 | 93 | if( !is_bool( $includeCert ) ) { 94 | throw new \InvalidArgumentException('$include_cert of register() method only accepts boolean.'); 95 | } 96 | 97 | // Unpack the registration data coming from the client-side token 98 | $rawRegistration = static::base64u_decode($response->registrationData); 99 | $registrationData = array_values(unpack('C*', $rawRegistration)); 100 | $clientData = static::base64u_decode($response->clientData); 101 | $clientToken = json_decode($clientData); 102 | 103 | // Check Client's challenge matches the original request's challenge 104 | if($clientToken->challenge !== $request->challenge()) { 105 | throw new U2FException( 106 | 'Registration challenge does not match', 107 | U2FException::UNMATCHED_CHALLENGE 108 | ); 109 | } 110 | 111 | // Begin validating and building the registration 112 | $registration = new Registration(); 113 | $offset = 1; 114 | $pubKey = substr($rawRegistration, $offset, static::PUBKEY_LEN); 115 | $offset += static::PUBKEY_LEN; 116 | 117 | // Validate and set the public key 118 | if(static::publicKeyToPem($pubKey) === null) { 119 | throw new U2FException( 120 | 'Decoding of public key failed', 121 | U2FException::PUBKEY_DECODE 122 | ); 123 | } 124 | $registration->setPublicKey(base64_encode($pubKey)); 125 | 126 | // Build and set the key handle. 127 | $keyHandleLength = $registrationData[$offset++]; 128 | $keyHandle = substr($rawRegistration, $offset, $keyHandleLength); 129 | $offset += $keyHandleLength; 130 | $registration->setKeyHandle(static::base64u_encode($keyHandle)); 131 | 132 | // Build certificate 133 | // Set certificate length 134 | // Note: length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes) 135 | $certLength = 4; 136 | $certLength += ($registrationData[$offset + 2] << 8); 137 | $certLength += $registrationData[$offset + 3]; 138 | 139 | // Write the certificate from the returning registration data 140 | $rawCert = static::fixSignatureUnusedBits(substr($rawRegistration, $offset, $certLength)); 141 | $offset += $certLength; 142 | $pemCert = "-----BEGIN CERTIFICATE-----\r\n"; 143 | $pemCert .= chunk_split(base64_encode($rawCert), 64); 144 | $pemCert .= "-----END CERTIFICATE-----"; 145 | if($includeCert) { 146 | $registration->setCertificate( base64_encode($rawCert) ); 147 | } 148 | 149 | // If we've set the attestDir, check the given certificate can be used. 150 | if($attestDir) { 151 | if(openssl_x509_checkpurpose($pemCert, -1, static::get_certs($attestDir)) !== true) { 152 | throw new U2FException( 153 | 'Attestation certificate can not be validated', 154 | U2FException::ATTESTATION_VERIFICATION 155 | ); 156 | } 157 | } 158 | 159 | // Attempt to extract public key from the certificate, if we can't something went wrong in making it. 160 | if(!openssl_pkey_get_public($pemCert)) { 161 | throw new U2FException( 162 | 'Decoding of public key failed', 163 | U2FException::PUBKEY_DECODE 164 | ); 165 | } 166 | 167 | // Generate signature from the remaining part of the raw registration data 168 | $signature = substr($rawRegistration, $offset); 169 | 170 | // Build a verification string from the components we've made in this function 171 | $dataToVerify = chr(0); 172 | $dataToVerify .= hash('sha256', $request->appId(), true); 173 | $dataToVerify .= hash('sha256', $clientData, true); 174 | $dataToVerify .= $keyHandle; 175 | $dataToVerify .= $pubKey; 176 | 177 | // Verify our data against the signature and the certificate, on success return the registration object 178 | if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) { 179 | return $registration; 180 | } else { 181 | throw new U2FException( 182 | 'Attestation signature does not match', 183 | U2FException::ATTESTATION_SIGNATURE 184 | ); 185 | } 186 | } 187 | 188 | /** 189 | * Called to get an authentication request. 190 | * 191 | * @param array $registrations An array of the registrations to create authentication requests for. 192 | * @param string $appId Application id for the running application, Basically the app's URL 193 | * @return array An array of SignRequest 194 | * @throws \InvalidArgumentException 195 | */ 196 | public static function makeAuthentication(array $registrations, $appId) 197 | { 198 | $signatures = []; 199 | $challenge = static::createChallenge(); 200 | foreach ($registrations as $reg) { 201 | if( !is_object( $reg ) ) { 202 | throw new \InvalidArgumentException('$registrations of makeAuthentication() method only accepts array of object.'); 203 | } 204 | 205 | $signatures[] = new SignRequest([ 206 | 'appId' => $appId, 207 | 'keyHandle' => $reg->keyHandle, 208 | 'challenge' => $challenge, 209 | ]); 210 | } 211 | return $signatures; 212 | } 213 | 214 | /** 215 | * Called to verify an authentication response 216 | * 217 | * @param array $requests An array of outstanding authentication requests 218 | * @param array $registrations An array of current registrations 219 | * @param object $response A response from the authenticator 220 | * @return \stdClass 221 | * @throws U2FException 222 | * 223 | * The Registration object returned on success contains an updated counter 224 | * that should be saved for future authentications. 225 | * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of 226 | * token cloning or similar and appropriate action should be taken. 227 | */ 228 | public static function authenticate(array $requests, array $registrations, $response) 229 | { 230 | // Parameter checks 231 | if( !is_object( $response ) ) { 232 | throw new \InvalidArgumentException('$response of authenticate() method only accepts object.'); 233 | } 234 | 235 | if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { 236 | throw new U2FException( 237 | 'User-agent returned error. Error code: ' . $response->errorCode, 238 | U2FException::BAD_UA_RETURNING 239 | ); 240 | } 241 | 242 | // Set default values to null, so we get fails by default 243 | /** @var object|null $req */ 244 | $req = null; 245 | 246 | /** @var object|null $reg */ 247 | $reg = null; 248 | 249 | // Extract client response data 250 | $clientData = static::base64u_decode($response->clientData); 251 | $decodedClient = json_decode($clientData); 252 | 253 | // Check we have a match among the requests and the response 254 | foreach ($requests as $req) { 255 | if( !is_object( $req ) ) { 256 | throw new \InvalidArgumentException('$requests of authenticate() method only accepts an array of objects.'); 257 | } 258 | 259 | if($req->keyHandle() === $response->keyHandle && $req->challenge() === $decodedClient->challenge) { 260 | break; 261 | } 262 | 263 | $req = null; 264 | } 265 | if($req === null) { 266 | throw new U2FException( 267 | 'No matching request found', 268 | U2FException::NO_MATCHING_REQUEST 269 | ); 270 | } 271 | 272 | // Check for a match for the response among a list of registrations 273 | foreach ($registrations as $reg) { 274 | if( !is_object( $reg ) ) { 275 | throw new \InvalidArgumentException('$registrations of authenticate() method only accepts an array of objects.'); 276 | } 277 | 278 | if($reg->keyHandle === $response->keyHandle) { 279 | break; 280 | } 281 | $reg = null; 282 | } 283 | if($reg === null) { 284 | throw new U2FException( 285 | 'No matching registration found', 286 | U2FException::NO_MATCHING_REGISTRATION 287 | ); 288 | } 289 | 290 | // On Success, check we have a valid public key 291 | $pemKey = static::publicKeyToPem(static::base64u_decode($reg->publicKey)); 292 | if($pemKey === null) { 293 | throw new U2FException( 294 | 'Decoding of public key failed', 295 | U2FException::PUBKEY_DECODE 296 | ); 297 | } 298 | 299 | // Build signature and data from response 300 | $signData = static::base64u_decode($response->signatureData); 301 | $dataToVerify = hash('sha256', $req->appId(), true); 302 | $dataToVerify .= substr($signData, 0, 5); 303 | $dataToVerify .= hash('sha256', $clientData, true); 304 | $signature = substr($signData, 5); 305 | 306 | // Verify the response data against the public key 307 | if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) { 308 | $ctr = unpack("Nctr", substr($signData, 1, 4)); 309 | $counter = $ctr['ctr']; 310 | /* TODO: wrap-around should be handled somehow.. */ 311 | if($counter > $reg->counter) { 312 | $reg->counter = $counter; 313 | return $reg; 314 | } else { 315 | throw new U2FException( 316 | 'Counter too low.', 317 | U2FException::COUNTER_TOO_LOW 318 | ); 319 | } 320 | } else { 321 | throw new U2FException( 322 | 'Authentication failed', 323 | U2FException::AUTHENTICATION_FAILURE 324 | ); 325 | } 326 | } 327 | 328 | /** 329 | * @param string $attestDir 330 | * @return array 331 | */ 332 | private static function get_certs($attestDir) 333 | { 334 | $files = []; 335 | $dir = $attestDir; 336 | if($dir && $handle = opendir($dir)) { 337 | while(false !== ($entry = readdir($handle))) { 338 | if(is_file("$dir/$entry")) { 339 | $files[] = "$dir/$entry"; 340 | } 341 | } 342 | closedir($handle); 343 | } 344 | return $files; 345 | } 346 | 347 | /** 348 | * @param string $data 349 | * @return string 350 | */ 351 | private static function base64u_encode($data) 352 | { 353 | return trim(strtr(base64_encode($data), '+/', '-_'), '='); 354 | } 355 | 356 | /** 357 | * @param string $data 358 | * @return string 359 | */ 360 | private static function base64u_decode($data) 361 | { 362 | return base64_decode(strtr($data, '-_', '+/')); 363 | } 364 | 365 | /** 366 | * @param string $key 367 | * @return null|string 368 | */ 369 | private static function publicKeyToPem($key) 370 | { 371 | if(strlen($key) !== static::PUBKEY_LEN || $key[0] !== "\x04") { 372 | return null; 373 | } 374 | 375 | /* 376 | * Convert the public key to binary DER format first 377 | * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 378 | * 379 | * SEQUENCE(2 elem) 30 59 380 | * SEQUENCE(2 elem) 30 13 381 | * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 382 | * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 383 | * BIT STRING(520 bit) 03 42 ..key.. 384 | */ 385 | $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01"; 386 | $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42"; 387 | $der .= "\0".$key; 388 | 389 | $pem = "-----BEGIN PUBLIC KEY-----\r\n"; 390 | $pem .= chunk_split(base64_encode($der), 64); 391 | $pem .= "-----END PUBLIC KEY-----"; 392 | 393 | return $pem; 394 | } 395 | 396 | /** 397 | * @return string 398 | * @throws U2FException 399 | */ 400 | private static function createChallenge() 401 | { 402 | $challenge = openssl_random_pseudo_bytes(32, $crypto_strong ); 403 | if( $crypto_strong !== true ) { 404 | throw new U2FException( 405 | 'Unable to obtain a good source of randomness', 406 | U2FException::BAD_RANDOM 407 | ); 408 | } 409 | 410 | $challenge = static::base64u_encode( $challenge ); 411 | 412 | return $challenge; 413 | } 414 | 415 | /** 416 | * Fixes a certificate where the signature contains unused bits. 417 | * 418 | * @param string $cert 419 | * @return mixed 420 | */ 421 | private static function fixSignatureUnusedBits($cert) 422 | { 423 | if(in_array(hash('sha256', $cert), static::$FIXCERTS)) { 424 | $cert[strlen($cert) - 257] = "\0"; 425 | } 426 | return $cert; 427 | } 428 | 429 | } 430 | --------------------------------------------------------------------------------