├── .gitignore ├── .travis.yml ├── BLURB ├── COPYING ├── NEWS ├── README ├── README.adoc ├── apigen.neon ├── composer.json ├── do-source-release.sh ├── examples ├── assets │ └── u2f-api.js ├── cli │ └── u2f-server.phps ├── localstorage │ └── index.phps └── pdo │ └── index.phps ├── phpunit.xml ├── psalm.xml ├── src └── u2flib_server │ └── U2F.php └── tests ├── certs └── yubico-u2f-ca-1.pem └── u2flib_test.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | .*.swp 4 | php-u2flib-server-*.tar.gz 5 | php-u2flib-server-*.tar.gz.sig 6 | apidocs/ 7 | build/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | - hhvm 8 | matrix: 9 | include: 10 | - php: 5.6 11 | env: COVERALLS=true 12 | allow_failures: 13 | - php: hhvm 14 | 15 | before_script: 16 | - composer install 17 | 18 | script: 19 | - ./vendor/bin/psalm 20 | - ./vendor/phpunit/phpunit/phpunit -c phpunit.xml 21 | 22 | after_success: 23 | - test -z $COVERALLS || (composer require satooshi/php-coveralls && vendor/bin/coveralls -v) 24 | -------------------------------------------------------------------------------- /BLURB: -------------------------------------------------------------------------------- 1 | Author: Yubico 2 | Basename: php-u2flib-server 3 | Homepage: https://developers.yubico.com/php-u2flib-server 4 | License: BSD-2-Clause 5 | Name: Native U2F library in PHP 6 | Project: php-u2flib-server 7 | Summary: Native U2F library in PHP 8 | Yubico-Category: U2F projects 9 | Travis: https://travis-ci.org/Yubico/php-u2flib-server 10 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Yubico AB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | php-u2flib-server NEWS -- History of user-visible changes. 2 | 3 | * Version 1.0.3 (unreleased) 4 | 5 | * Version 1.0.2 (released 2018-09-07) 6 | ** Additional error checks. 7 | ** Add user presence check. 8 | ** Support single files for attestation root. 9 | ** Type safety, CSPRNG, avoid chr(). 10 | 11 | * Version 1.0.1 (released 2017-05-09) 12 | ** Move examples to phps so they don't execute by default 13 | ** Use common challenge for multiple registrations 14 | 15 | * Version 1.0.0 (released 2016-02-19) 16 | ** Give an early error on openssl < 1.0 17 | ** Support devices with initial counter 0 18 | ** Fixes to examples 19 | ** Handle errorCode: 0 correctly 20 | 21 | * Version 0.1.0 (released 2015-03-03) 22 | ** Use openssl for all crypto instead of third party extensions. 23 | ** Properly check the request challenge on authenticate. 24 | ** Switch from returning error codes to throwing exceptions. 25 | ** Stop recommending composer for installation. 26 | 27 | * Version 0.0.2 (released 2014-10-24) 28 | ** Refactor the API to return objects instead of encoded objects. 29 | ** Add a second example that uses PDO to store registrations. 30 | ** Add documentation to the API. 31 | ** Check that randomness returned is good. 32 | ** Drop the unneeded mcrypt extension. 33 | ** More tests. 34 | 35 | * Version 0.0.1 (released 2014-10-16) 36 | ** Initial release. 37 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | php-u2flib-server 2 | ----------------- 3 | 4 | NOTE: _OBSOLETE: This project is no longer maintained. 5 | U2F has been superseded by https://www.w3.org/TR/webauthn/[Web Authentication]. 6 | We recommend using WebAuthn instead._ 7 | 8 | image:https://travis-ci.org/Yubico/php-u2flib-server.svg?branch=master["Build Status", link="https://travis-ci.org/Yubico/php-u2flib-server"] 9 | image:https://coveralls.io/repos/Yubico/php-u2flib-server/badge.svg?branch=master&service=github["Coverage", link="https://coveralls.io/github/Yubico/php-u2flib-server?branch=master"] 10 | image:https://scrutinizer-ci.com/g/Yubico/php-u2flib-server/badges/quality-score.png?b=master["Scrutinizer Code Quality", link="https://scrutinizer-ci.com/g/Yubico/php-u2flib-server/?branch=master"] 11 | 12 | === Introduction === 13 | 14 | Serverside U2F library for PHP. Provides functionality for registering 15 | tokens and authentication with said tokens. 16 | 17 | To read more about U2F and how to use a U2F library, visit 18 | link:http://developers.yubico.com/U2F[developers.yubico.com/U2F]. 19 | 20 | === License === 21 | 22 | The project is licensed under a BSD license. See the file COPYING for 23 | exact wording. For any copyright year range specified as YYYY-ZZZZ in 24 | this package note that the range specifies every single year in that 25 | closed interval. 26 | 27 | === Dependencies === 28 | 29 | The only dependency is the openssl extension to PHP that has to be enabled. 30 | 31 | A composer.json is included in the distribution to make things simpler for 32 | other project using composer. 33 | 34 | === Tests === 35 | 36 | To run the test suite link:https://phpunit.de[PHPUnit] is required. To run it, type: 37 | 38 | $ phpunit 39 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /apigen.neon: -------------------------------------------------------------------------------- 1 | destination: apidocs 2 | 3 | source: 4 | - src/u2flib_server 5 | 6 | exclude: 7 | - "*/tests/*" 8 | 9 | groups: none 10 | 11 | tree: false 12 | 13 | title: php-u2flib-server API 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"yubico/u2flib-server", 3 | "description":"Library for U2F implementation", 4 | "homepage":"https://developers.yubico.com/php-u2flib-server", 5 | "license":"BSD-2-Clause", 6 | "require": { 7 | "ext-openssl":"*", 8 | "paragonie/random_compat": ">= 1", 9 | "php": ">=5.6" 10 | }, 11 | "autoload": { 12 | "classmap": ["src/"] 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "~5.7", 16 | "vimeo/psalm": "^0|^1|^2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /do-source-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | VERSION=$1 6 | PGP_KEYID=$2 7 | 8 | if [ "x$PGP_KEYID" = "x" ]; then 9 | echo "try with $0 VERSION PGP_KEYID" 10 | echo "example: $0 0.0.1 B2168C0A" 11 | exit 12 | fi 13 | 14 | if ! head -3 NEWS | grep -q "Version $VERSION .released `date -I`"; then 15 | echo "You need to update date/version in NEWS" 16 | exit 17 | fi 18 | 19 | if [ "x$YUBICO_GITHUB_REPO" = "x" ]; then 20 | echo "you need to define YUBICO_GITHUB_REPO" 21 | exit 22 | fi 23 | 24 | releasename=php-u2flib-server-${VERSION} 25 | 26 | git push 27 | git tag -u ${PGP_KEYID} -m $VERSION $VERSION 28 | git push --tags 29 | tmpdir=`mktemp -d /tmp/release.XXXXXX` 30 | releasedir=${tmpdir}/${releasename} 31 | mkdir -p $releasedir 32 | git archive $VERSION --format=tar | tar -xC $releasedir 33 | git2cl > $releasedir/ChangeLog 34 | cd $releasedir 35 | apigen generate 36 | cd - 37 | tar -cz --directory=$tmpdir --file=${releasename}.tar.gz $releasename 38 | gpg --detach-sign --default-key $PGP_KEYID ${releasename}.tar.gz 39 | $YUBICO_GITHUB_REPO/publish php-u2flib-server $VERSION ${releasename}.tar.gz* 40 | rm -rf $tmpdir 41 | -------------------------------------------------------------------------------- /examples/assets/u2f-api.js: -------------------------------------------------------------------------------- 1 | //Copyright 2014-2015 Google Inc. All rights reserved. 2 | 3 | //Use of this source code is governed by a BSD-style 4 | //license that can be found in the LICENSE file or at 5 | //https://developers.google.com/open-source/licenses/bsd 6 | 7 | /** 8 | * @fileoverview The U2F api. 9 | */ 10 | 'use strict'; 11 | 12 | 13 | /** 14 | * Namespace for the U2F api. 15 | * @type {Object} 16 | */ 17 | var u2f = u2f || {}; 18 | 19 | /** 20 | * FIDO U2F Javascript API Version 21 | * @number 22 | */ 23 | var js_api_version; 24 | 25 | /** 26 | * The U2F extension id 27 | * @const {string} 28 | */ 29 | // The Chrome packaged app extension ID. 30 | // Uncomment this if you want to deploy a server instance that uses 31 | // the package Chrome app and does not require installing the U2F Chrome extension. 32 | u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; 33 | // The U2F Chrome extension ID. 34 | // Uncomment this if you want to deploy a server instance that uses 35 | // the U2F Chrome extension to authenticate. 36 | // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; 37 | 38 | 39 | /** 40 | * Message types for messsages to/from the extension 41 | * @const 42 | * @enum {string} 43 | */ 44 | u2f.MessageTypes = { 45 | 'U2F_REGISTER_REQUEST': 'u2f_register_request', 46 | 'U2F_REGISTER_RESPONSE': 'u2f_register_response', 47 | 'U2F_SIGN_REQUEST': 'u2f_sign_request', 48 | 'U2F_SIGN_RESPONSE': 'u2f_sign_response', 49 | 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', 50 | 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' 51 | }; 52 | 53 | 54 | /** 55 | * Response status codes 56 | * @const 57 | * @enum {number} 58 | */ 59 | u2f.ErrorCodes = { 60 | 'OK': 0, 61 | 'OTHER_ERROR': 1, 62 | 'BAD_REQUEST': 2, 63 | 'CONFIGURATION_UNSUPPORTED': 3, 64 | 'DEVICE_INELIGIBLE': 4, 65 | 'TIMEOUT': 5 66 | }; 67 | 68 | 69 | /** 70 | * A message for registration requests 71 | * @typedef {{ 72 | * type: u2f.MessageTypes, 73 | * appId: ?string, 74 | * timeoutSeconds: ?number, 75 | * requestId: ?number 76 | * }} 77 | */ 78 | u2f.U2fRequest; 79 | 80 | 81 | /** 82 | * A message for registration responses 83 | * @typedef {{ 84 | * type: u2f.MessageTypes, 85 | * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), 86 | * requestId: ?number 87 | * }} 88 | */ 89 | u2f.U2fResponse; 90 | 91 | 92 | /** 93 | * An error object for responses 94 | * @typedef {{ 95 | * errorCode: u2f.ErrorCodes, 96 | * errorMessage: ?string 97 | * }} 98 | */ 99 | u2f.Error; 100 | 101 | /** 102 | * Data object for a single sign request. 103 | * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC, USB_INTERNAL}} 104 | */ 105 | u2f.Transport; 106 | 107 | 108 | /** 109 | * Data object for a single sign request. 110 | * @typedef {Array} 111 | */ 112 | u2f.Transports; 113 | 114 | /** 115 | * Data object for a single sign request. 116 | * @typedef {{ 117 | * version: string, 118 | * challenge: string, 119 | * keyHandle: string, 120 | * appId: string 121 | * }} 122 | */ 123 | u2f.SignRequest; 124 | 125 | 126 | /** 127 | * Data object for a sign response. 128 | * @typedef {{ 129 | * keyHandle: string, 130 | * signatureData: string, 131 | * clientData: string 132 | * }} 133 | */ 134 | u2f.SignResponse; 135 | 136 | 137 | /** 138 | * Data object for a registration request. 139 | * @typedef {{ 140 | * version: string, 141 | * challenge: string 142 | * }} 143 | */ 144 | u2f.RegisterRequest; 145 | 146 | 147 | /** 148 | * Data object for a registration response. 149 | * @typedef {{ 150 | * version: string, 151 | * keyHandle: string, 152 | * transports: Transports, 153 | * appId: string 154 | * }} 155 | */ 156 | u2f.RegisterResponse; 157 | 158 | 159 | /** 160 | * Data object for a registered key. 161 | * @typedef {{ 162 | * version: string, 163 | * keyHandle: string, 164 | * transports: ?Transports, 165 | * appId: ?string 166 | * }} 167 | */ 168 | u2f.RegisteredKey; 169 | 170 | 171 | /** 172 | * Data object for a get API register response. 173 | * @typedef {{ 174 | * js_api_version: number 175 | * }} 176 | */ 177 | u2f.GetJsApiVersionResponse; 178 | 179 | 180 | //Low level MessagePort API support 181 | 182 | /** 183 | * Sets up a MessagePort to the U2F extension using the 184 | * available mechanisms. 185 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 186 | */ 187 | u2f.getMessagePort = function(callback) { 188 | if (typeof chrome != 'undefined' && chrome.runtime) { 189 | // The actual message here does not matter, but we need to get a reply 190 | // for the callback to run. Thus, send an empty signature request 191 | // in order to get a failure response. 192 | var msg = { 193 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 194 | signRequests: [] 195 | }; 196 | chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { 197 | if (!chrome.runtime.lastError) { 198 | // We are on a whitelisted origin and can talk directly 199 | // with the extension. 200 | u2f.getChromeRuntimePort_(callback); 201 | } else { 202 | // chrome.runtime was available, but we couldn't message 203 | // the extension directly, use iframe 204 | u2f.getIframePort_(callback); 205 | } 206 | }); 207 | } else if (u2f.isAndroidChrome_()) { 208 | u2f.getAuthenticatorPort_(callback); 209 | } else if (u2f.isIosChrome_()) { 210 | u2f.getIosPort_(callback); 211 | } else { 212 | // chrome.runtime was not available at all, which is normal 213 | // when this origin doesn't have access to any extensions. 214 | u2f.getIframePort_(callback); 215 | } 216 | }; 217 | 218 | /** 219 | * Detect chrome running on android based on the browser's useragent. 220 | * @private 221 | */ 222 | u2f.isAndroidChrome_ = function() { 223 | var userAgent = navigator.userAgent; 224 | return userAgent.indexOf('Chrome') != -1 && 225 | userAgent.indexOf('Android') != -1; 226 | }; 227 | 228 | /** 229 | * Detect chrome running on iOS based on the browser's platform. 230 | * @private 231 | */ 232 | u2f.isIosChrome_ = function() { 233 | return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1; 234 | }; 235 | 236 | /** 237 | * Connects directly to the extension via chrome.runtime.connect. 238 | * @param {function(u2f.WrappedChromeRuntimePort_)} callback 239 | * @private 240 | */ 241 | u2f.getChromeRuntimePort_ = function(callback) { 242 | var port = chrome.runtime.connect(u2f.EXTENSION_ID, 243 | {'includeTlsChannelId': true}); 244 | setTimeout(function() { 245 | callback(new u2f.WrappedChromeRuntimePort_(port)); 246 | }, 0); 247 | }; 248 | 249 | /** 250 | * Return a 'port' abstraction to the Authenticator app. 251 | * @param {function(u2f.WrappedAuthenticatorPort_)} callback 252 | * @private 253 | */ 254 | u2f.getAuthenticatorPort_ = function(callback) { 255 | setTimeout(function() { 256 | callback(new u2f.WrappedAuthenticatorPort_()); 257 | }, 0); 258 | }; 259 | 260 | /** 261 | * Return a 'port' abstraction to the iOS client app. 262 | * @param {function(u2f.WrappedIosPort_)} callback 263 | * @private 264 | */ 265 | u2f.getIosPort_ = function(callback) { 266 | setTimeout(function() { 267 | callback(new u2f.WrappedIosPort_()); 268 | }, 0); 269 | }; 270 | 271 | /** 272 | * A wrapper for chrome.runtime.Port that is compatible with MessagePort. 273 | * @param {Port} port 274 | * @constructor 275 | * @private 276 | */ 277 | u2f.WrappedChromeRuntimePort_ = function(port) { 278 | this.port_ = port; 279 | }; 280 | 281 | /** 282 | * Format and return a sign request compliant with the JS API version supported by the extension. 283 | * @param {Array} signRequests 284 | * @param {number} timeoutSeconds 285 | * @param {number} reqId 286 | * @return {Object} 287 | */ 288 | u2f.formatSignRequest_ = 289 | function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { 290 | if (js_api_version === undefined || js_api_version < 1.1) { 291 | // Adapt request to the 1.0 JS API 292 | var signRequests = []; 293 | for (var i = 0; i < registeredKeys.length; i++) { 294 | signRequests[i] = { 295 | version: registeredKeys[i].version, 296 | challenge: challenge, 297 | keyHandle: registeredKeys[i].keyHandle, 298 | appId: appId 299 | }; 300 | } 301 | return { 302 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 303 | signRequests: signRequests, 304 | timeoutSeconds: timeoutSeconds, 305 | requestId: reqId 306 | }; 307 | } 308 | // JS 1.1 API 309 | return { 310 | type: u2f.MessageTypes.U2F_SIGN_REQUEST, 311 | appId: appId, 312 | challenge: challenge, 313 | registeredKeys: registeredKeys, 314 | timeoutSeconds: timeoutSeconds, 315 | requestId: reqId 316 | }; 317 | }; 318 | 319 | /** 320 | * Format and return a register request compliant with the JS API version supported by the extension.. 321 | * @param {Array} signRequests 322 | * @param {Array} signRequests 323 | * @param {number} timeoutSeconds 324 | * @param {number} reqId 325 | * @return {Object} 326 | */ 327 | u2f.formatRegisterRequest_ = 328 | function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { 329 | if (js_api_version === undefined || js_api_version < 1.1) { 330 | // Adapt request to the 1.0 JS API 331 | for (var i = 0; i < registerRequests.length; i++) { 332 | registerRequests[i].appId = appId; 333 | } 334 | var signRequests = []; 335 | for (var i = 0; i < registeredKeys.length; i++) { 336 | signRequests[i] = { 337 | version: registeredKeys[i].version, 338 | challenge: registerRequests[0], 339 | keyHandle: registeredKeys[i].keyHandle, 340 | appId: appId 341 | }; 342 | } 343 | return { 344 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 345 | signRequests: signRequests, 346 | registerRequests: registerRequests, 347 | timeoutSeconds: timeoutSeconds, 348 | requestId: reqId 349 | }; 350 | } 351 | // JS 1.1 API 352 | return { 353 | type: u2f.MessageTypes.U2F_REGISTER_REQUEST, 354 | appId: appId, 355 | registerRequests: registerRequests, 356 | registeredKeys: registeredKeys, 357 | timeoutSeconds: timeoutSeconds, 358 | requestId: reqId 359 | }; 360 | }; 361 | 362 | 363 | /** 364 | * Posts a message on the underlying channel. 365 | * @param {Object} message 366 | */ 367 | u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { 368 | this.port_.postMessage(message); 369 | }; 370 | 371 | 372 | /** 373 | * Emulates the HTML 5 addEventListener interface. Works only for the 374 | * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. 375 | * @param {string} eventName 376 | * @param {function({data: Object})} handler 377 | */ 378 | u2f.WrappedChromeRuntimePort_.prototype.addEventListener = 379 | function(eventName, handler) { 380 | var name = eventName.toLowerCase(); 381 | if (name == 'message' || name == 'onmessage') { 382 | this.port_.onMessage.addListener(function(message) { 383 | // Emulate a minimal MessageEvent object 384 | handler({'data': message}); 385 | }); 386 | } else { 387 | console.error('WrappedChromeRuntimePort only supports onMessage'); 388 | } 389 | }; 390 | 391 | /** 392 | * Wrap the Authenticator app with a MessagePort interface. 393 | * @constructor 394 | * @private 395 | */ 396 | u2f.WrappedAuthenticatorPort_ = function() { 397 | this.requestId_ = -1; 398 | this.requestObject_ = null; 399 | } 400 | 401 | /** 402 | * Launch the Authenticator intent. 403 | * @param {Object} message 404 | */ 405 | u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { 406 | var intentUrl = 407 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + 408 | ';S.request=' + encodeURIComponent(JSON.stringify(message)) + 409 | ';end'; 410 | document.location = intentUrl; 411 | }; 412 | 413 | /** 414 | * Tells what type of port this is. 415 | * @return {String} port type 416 | */ 417 | u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { 418 | return "WrappedAuthenticatorPort_"; 419 | }; 420 | 421 | 422 | /** 423 | * Emulates the HTML 5 addEventListener interface. 424 | * @param {string} eventName 425 | * @param {function({data: Object})} handler 426 | */ 427 | u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { 428 | var name = eventName.toLowerCase(); 429 | if (name == 'message') { 430 | var self = this; 431 | /* Register a callback to that executes when 432 | * chrome injects the response. */ 433 | window.addEventListener( 434 | 'message', self.onRequestUpdate_.bind(self, handler), false); 435 | } else { 436 | console.error('WrappedAuthenticatorPort only supports message'); 437 | } 438 | }; 439 | 440 | /** 441 | * Callback invoked when a response is received from the Authenticator. 442 | * @param function({data: Object}) callback 443 | * @param {Object} message message Object 444 | */ 445 | u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = 446 | function(callback, message) { 447 | var messageObject = JSON.parse(message.data); 448 | var intentUrl = messageObject['intentURL']; 449 | 450 | var errorCode = messageObject['errorCode']; 451 | var responseObject = null; 452 | if (messageObject.hasOwnProperty('data')) { 453 | responseObject = /** @type {Object} */ ( 454 | JSON.parse(messageObject['data'])); 455 | } 456 | 457 | callback({'data': responseObject}); 458 | }; 459 | 460 | /** 461 | * Base URL for intents to Authenticator. 462 | * @const 463 | * @private 464 | */ 465 | u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = 466 | 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; 467 | 468 | /** 469 | * Wrap the iOS client app with a MessagePort interface. 470 | * @constructor 471 | * @private 472 | */ 473 | u2f.WrappedIosPort_ = function() {}; 474 | 475 | /** 476 | * Launch the iOS client app request 477 | * @param {Object} message 478 | */ 479 | u2f.WrappedIosPort_.prototype.postMessage = function(message) { 480 | var str = JSON.stringify(message); 481 | var url = "u2f://auth?" + encodeURI(str); 482 | location.replace(url); 483 | }; 484 | 485 | /** 486 | * Tells what type of port this is. 487 | * @return {String} port type 488 | */ 489 | u2f.WrappedIosPort_.prototype.getPortType = function() { 490 | return "WrappedIosPort_"; 491 | }; 492 | 493 | /** 494 | * Emulates the HTML 5 addEventListener interface. 495 | * @param {string} eventName 496 | * @param {function({data: Object})} handler 497 | */ 498 | u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { 499 | var name = eventName.toLowerCase(); 500 | if (name !== 'message') { 501 | console.error('WrappedIosPort only supports message'); 502 | } 503 | }; 504 | 505 | /** 506 | * Sets up an embedded trampoline iframe, sourced from the extension. 507 | * @param {function(MessagePort)} callback 508 | * @private 509 | */ 510 | u2f.getIframePort_ = function(callback) { 511 | // Create the iframe 512 | var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; 513 | var iframe = document.createElement('iframe'); 514 | iframe.src = iframeOrigin + '/u2f-comms.html'; 515 | iframe.setAttribute('style', 'display:none'); 516 | document.body.appendChild(iframe); 517 | 518 | var channel = new MessageChannel(); 519 | var ready = function(message) { 520 | if (message.data == 'ready') { 521 | channel.port1.removeEventListener('message', ready); 522 | callback(channel.port1); 523 | } else { 524 | console.error('First event on iframe port was not "ready"'); 525 | } 526 | }; 527 | channel.port1.addEventListener('message', ready); 528 | channel.port1.start(); 529 | 530 | iframe.addEventListener('load', function() { 531 | // Deliver the port to the iframe and initialize 532 | iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); 533 | }); 534 | }; 535 | 536 | 537 | //High-level JS API 538 | 539 | /** 540 | * Default extension response timeout in seconds. 541 | * @const 542 | */ 543 | u2f.EXTENSION_TIMEOUT_SEC = 30; 544 | 545 | /** 546 | * A singleton instance for a MessagePort to the extension. 547 | * @type {MessagePort|u2f.WrappedChromeRuntimePort_} 548 | * @private 549 | */ 550 | u2f.port_ = null; 551 | 552 | /** 553 | * Callbacks waiting for a port 554 | * @type {Array} 555 | * @private 556 | */ 557 | u2f.waitingForPort_ = []; 558 | 559 | /** 560 | * A counter for requestIds. 561 | * @type {number} 562 | * @private 563 | */ 564 | u2f.reqCounter_ = 0; 565 | 566 | /** 567 | * A map from requestIds to client callbacks 568 | * @type {Object.} 570 | * @private 571 | */ 572 | u2f.callbackMap_ = {}; 573 | 574 | /** 575 | * Creates or retrieves the MessagePort singleton to use. 576 | * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback 577 | * @private 578 | */ 579 | u2f.getPortSingleton_ = function(callback) { 580 | if (u2f.port_) { 581 | callback(u2f.port_); 582 | } else { 583 | if (u2f.waitingForPort_.length == 0) { 584 | u2f.getMessagePort(function(port) { 585 | u2f.port_ = port; 586 | u2f.port_.addEventListener('message', 587 | /** @type {function(Event)} */ (u2f.responseHandler_)); 588 | 589 | // Careful, here be async callbacks. Maybe. 590 | while (u2f.waitingForPort_.length) 591 | u2f.waitingForPort_.shift()(u2f.port_); 592 | }); 593 | } 594 | u2f.waitingForPort_.push(callback); 595 | } 596 | }; 597 | 598 | /** 599 | * Handles response messages from the extension. 600 | * @param {MessageEvent.} message 601 | * @private 602 | */ 603 | u2f.responseHandler_ = function(message) { 604 | var response = message.data; 605 | var reqId = response['requestId']; 606 | if (!reqId || !u2f.callbackMap_[reqId]) { 607 | console.error('Unknown or missing requestId in response.'); 608 | return; 609 | } 610 | var cb = u2f.callbackMap_[reqId]; 611 | delete u2f.callbackMap_[reqId]; 612 | cb(response['responseData']); 613 | }; 614 | 615 | /** 616 | * Dispatches an array of sign requests to available U2F tokens. 617 | * If the JS API version supported by the extension is unknown, it first sends a 618 | * message to the extension to find out the supported API version and then it sends 619 | * the sign request. 620 | * @param {string=} appId 621 | * @param {string=} challenge 622 | * @param {Array} registeredKeys 623 | * @param {function((u2f.Error|u2f.SignResponse))} callback 624 | * @param {number=} opt_timeoutSeconds 625 | */ 626 | u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 627 | if (js_api_version === undefined) { 628 | // Send a message to get the extension to JS API version, then send the actual sign request. 629 | u2f.getApiVersion( 630 | function (response) { 631 | js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; 632 | console.log("Extension JS API Version: ", js_api_version); 633 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 634 | }); 635 | } else { 636 | // We know the JS API version. Send the actual sign request in the supported API version. 637 | u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); 638 | } 639 | }; 640 | 641 | /** 642 | * Dispatches an array of sign requests to available U2F tokens. 643 | * @param {string=} appId 644 | * @param {string=} challenge 645 | * @param {Array} registeredKeys 646 | * @param {function((u2f.Error|u2f.SignResponse))} callback 647 | * @param {number=} opt_timeoutSeconds 648 | */ 649 | u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { 650 | u2f.getPortSingleton_(function(port) { 651 | var reqId = ++u2f.reqCounter_; 652 | u2f.callbackMap_[reqId] = callback; 653 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 654 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 655 | var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); 656 | port.postMessage(req); 657 | }); 658 | }; 659 | 660 | /** 661 | * Dispatches register requests to available U2F tokens. An array of sign 662 | * requests identifies already registered tokens. 663 | * If the JS API version supported by the extension is unknown, it first sends a 664 | * message to the extension to find out the supported API version and then it sends 665 | * the register request. 666 | * @param {string=} appId 667 | * @param {Array} registerRequests 668 | * @param {Array} registeredKeys 669 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 670 | * @param {number=} opt_timeoutSeconds 671 | */ 672 | u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 673 | if (js_api_version === undefined) { 674 | // Send a message to get the extension to JS API version, then send the actual register request. 675 | u2f.getApiVersion( 676 | function (response) { 677 | js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; 678 | console.log("Extension JS API Version: ", js_api_version); 679 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 680 | callback, opt_timeoutSeconds); 681 | }); 682 | } else { 683 | // We know the JS API version. Send the actual register request in the supported API version. 684 | u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, 685 | callback, opt_timeoutSeconds); 686 | } 687 | }; 688 | 689 | /** 690 | * Dispatches register requests to available U2F tokens. An array of sign 691 | * requests identifies already registered tokens. 692 | * @param {string=} appId 693 | * @param {Array} registerRequests 694 | * @param {Array} registeredKeys 695 | * @param {function((u2f.Error|u2f.RegisterResponse))} callback 696 | * @param {number=} opt_timeoutSeconds 697 | */ 698 | u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { 699 | u2f.getPortSingleton_(function(port) { 700 | var reqId = ++u2f.reqCounter_; 701 | u2f.callbackMap_[reqId] = callback; 702 | var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? 703 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); 704 | var req = u2f.formatRegisterRequest_( 705 | appId, registeredKeys, registerRequests, timeoutSeconds, reqId); 706 | port.postMessage(req); 707 | }); 708 | }; 709 | 710 | 711 | /** 712 | * Dispatches a message to the extension to find out the supported 713 | * JS API version. 714 | * If the user is on a mobile phone and is thus using Google Authenticator instead 715 | * of the Chrome extension, don't send the request and simply return 0. 716 | * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback 717 | * @param {number=} opt_timeoutSeconds 718 | */ 719 | u2f.getApiVersion = function(callback, opt_timeoutSeconds) { 720 | u2f.getPortSingleton_(function(port) { 721 | // If we are using Android Google Authenticator or iOS client app, 722 | // do not fire an intent to ask which JS API version to use. 723 | if (port.getPortType) { 724 | var apiVersion; 725 | switch (port.getPortType()) { 726 | case 'WrappedIosPort_': 727 | case 'WrappedAuthenticatorPort_': 728 | apiVersion = 1.1; 729 | break; 730 | 731 | default: 732 | apiVersion = 0; 733 | break; 734 | } 735 | callback({ 'js_api_version': apiVersion }); 736 | return; 737 | } 738 | var reqId = ++u2f.reqCounter_; 739 | u2f.callbackMap_[reqId] = callback; 740 | var req = { 741 | type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, 742 | timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? 743 | opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), 744 | requestId: reqId 745 | }; 746 | port.postMessage(req); 747 | }); 748 | }; 749 | -------------------------------------------------------------------------------- /examples/cli/u2f-server.phps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | getRegisterData(); 68 | } elseif($mode === "authenticate") { 69 | $challenge = $u2f->getAuthenticateData($regs); 70 | } 71 | 72 | print json_encode($challenge[0]) . "\n"; 73 | $response = fgets(STDIN); 74 | 75 | if($mode === "register") { 76 | $result = $u2f->doRegister($challenge[0], json_decode($response)); 77 | } elseif($mode === "authenticate") { 78 | $result = $u2f->doAuthenticate($challenge, $regs, json_decode($response)); 79 | } 80 | 81 | print json_encode($result) . "\n"; 82 | 83 | ?> 84 | -------------------------------------------------------------------------------- /examples/localstorage/index.phps: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | PHP U2F Demo 44 | 45 | 46 | 47 | 159 | 160 | 161 | 162 |
163 | 164 | 165 | 166 | 167 | 168 | 169 |
170 | 171 |

172 | 0 Authenticators currently registered. 173 |

174 | 175 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /examples/pdo/index.phps: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 42 | $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); 43 | 44 | $pdo->exec("create table if not exists users (id integer primary key, name varchar(255))"); 45 | $pdo->exec("create table if not exists registrations (id integer primary key, user_id integer, keyHandle varchar(255), publicKey varchar(255), certificate text, counter integer)"); 46 | 47 | $scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://"; 48 | $u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']); 49 | 50 | session_start(); 51 | 52 | function createAndGetUser($name) { 53 | global $pdo; 54 | $sel = $pdo->prepare("select * from users where name = ?"); 55 | $sel->execute(array($name)); 56 | $user = $sel->fetch(); 57 | if(!$user) { 58 | $ins = $pdo->prepare("insert into users (name) values(?)"); 59 | $ins->execute(array($name)); 60 | $sel->execute(array($name)); 61 | $user = $sel->fetch(); 62 | } 63 | return $user; 64 | } 65 | 66 | function getRegs($user_id) { 67 | global $pdo; 68 | $sel = $pdo->prepare("select * from registrations where user_id = ?"); 69 | $sel->execute(array($user_id)); 70 | return $sel->fetchAll(); 71 | } 72 | 73 | function addReg($user_id, $reg) { 74 | global $pdo; 75 | $ins = $pdo->prepare("insert into registrations (user_id, keyHandle, publicKey, certificate, counter) values (?, ?, ?, ?, ?)"); 76 | $ins->execute(array($user_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter)); 77 | } 78 | 79 | function updateReg($reg) { 80 | global $pdo; 81 | $upd = $pdo->prepare("update registrations set counter = ? where keyHandle = ?"); 82 | $upd->execute(array($reg->counter, $reg->keyHandle)); 83 | } 84 | 85 | ?> 86 | 87 | 88 | 89 | PHP U2F example 90 | 91 | 92 | 93 | 199 | 200 | 201 | 202 |
203 | username:
204 | register:
205 | authenticate:
206 | 207 | 208 | 209 |
210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | tests 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/u2flib_server/U2F.php: -------------------------------------------------------------------------------- 1 | appId = $appId; 123 | $this->attestDir = $attestDir; 124 | 125 | if(!is_array($facetIds)) { 126 | $facetIds = [$appId]; 127 | } 128 | $this->facetIds = $facetIds; 129 | } 130 | 131 | /** 132 | * Called to get a registration request to send to a user. 133 | * Returns an array of one registration request and a array of sign requests. 134 | * 135 | * @param array $registrations List of current registrations for this 136 | * user, to prevent the user from registering the same authenticator several 137 | * times. 138 | * @return array An array of two elements, the first containing a 139 | * RegisterRequest the second being an array of SignRequest 140 | * @throws Error 141 | */ 142 | public function getRegisterData(array $registrations = array()) 143 | { 144 | $challenge = $this->createChallenge(); 145 | $request = new RegisterRequest($challenge, $this->appId); 146 | $signs = $this->getAuthenticateData($registrations); 147 | return array($request, $signs); 148 | } 149 | 150 | /** 151 | * Called to verify and unpack a registration message. 152 | * 153 | * @param RegisterRequest $request this is a reply to 154 | * @param object $response response from a user 155 | * @param bool $includeCert set to true if the attestation certificate should be 156 | * included in the returned Registration object 157 | * @return Registration 158 | * @throws Error 159 | */ 160 | public function doRegister($request, $response, $includeCert = true) 161 | { 162 | if( !is_object( $request ) ) { 163 | throw new \InvalidArgumentException('$request of doRegister() method only accepts object.'); 164 | } 165 | 166 | if( !is_object( $response ) ) { 167 | throw new \InvalidArgumentException('$response of doRegister() method only accepts object.'); 168 | } 169 | 170 | if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { 171 | throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); 172 | } 173 | 174 | if( !is_bool( $includeCert ) ) { 175 | throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.'); 176 | } 177 | 178 | $rawReg = $this->base64u_decode($response->registrationData); 179 | $regData = array_values(unpack('C*', $rawReg)); 180 | $clientData = $this->base64u_decode($response->clientData); 181 | $cli = json_decode($clientData); 182 | 183 | if($cli->challenge !== $request->challenge) { 184 | throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE ); 185 | } 186 | 187 | if(isset($cli->typ) && $cli->typ !== REQUEST_TYPE_REGISTER) { 188 | throw new Error('ClientData type is invalid', ERR_BAD_TYPE); 189 | } 190 | 191 | if(isset($cli->origin) && !in_array($cli->origin, $this->facetIds, true)) { 192 | throw new Error('App ID does not match the origin', ERR_NO_MATCHING_ORIGIN); 193 | } 194 | 195 | $registration = new Registration(); 196 | $offs = 1; 197 | $pubKey = substr($rawReg, $offs, PUBKEY_LEN); 198 | $offs += PUBKEY_LEN; 199 | // decode the pubKey to make sure it's good 200 | $tmpKey = $this->pubkey_to_pem($pubKey); 201 | if($tmpKey === null) { 202 | throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); 203 | } 204 | $registration->publicKey = base64_encode($pubKey); 205 | $khLen = $regData[$offs++]; 206 | $kh = substr($rawReg, $offs, $khLen); 207 | $offs += $khLen; 208 | $registration->keyHandle = $this->base64u_encode($kh); 209 | 210 | // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes) 211 | $certLen = 4; 212 | $certLen += ($regData[$offs + 2] << 8); 213 | $certLen += $regData[$offs + 3]; 214 | 215 | $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen)); 216 | $offs += $certLen; 217 | $pemCert = "-----BEGIN CERTIFICATE-----\r\n"; 218 | $pemCert .= chunk_split(base64_encode($rawCert), 64); 219 | $pemCert .= "-----END CERTIFICATE-----"; 220 | if($includeCert) { 221 | $registration->certificate = base64_encode($rawCert); 222 | } 223 | if($this->attestDir) { 224 | if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) { 225 | throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION ); 226 | } 227 | } 228 | 229 | if(!openssl_pkey_get_public($pemCert)) { 230 | throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); 231 | } 232 | $signature = substr($rawReg, $offs); 233 | 234 | $dataToVerify = pack('C', 0); 235 | $dataToVerify .= hash('sha256', $request->appId, true); 236 | $dataToVerify .= hash('sha256', $clientData, true); 237 | $dataToVerify .= $kh; 238 | $dataToVerify .= $pubKey; 239 | 240 | if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) { 241 | return $registration; 242 | } else { 243 | throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE ); 244 | } 245 | } 246 | 247 | /** 248 | * Called to get an authentication request. 249 | * 250 | * @param array $registrations An array of the registrations to create authentication requests for. 251 | * @return array An array of SignRequest 252 | * @throws Error 253 | */ 254 | public function getAuthenticateData(array $registrations) 255 | { 256 | $sigs = array(); 257 | $challenge = $this->createChallenge(); 258 | foreach ($registrations as $reg) { 259 | if( !is_object( $reg ) ) { 260 | throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.'); 261 | } 262 | /** @var Registration $reg */ 263 | 264 | $sig = new SignRequest(); 265 | $sig->appId = $this->appId; 266 | $sig->keyHandle = $reg->keyHandle; 267 | $sig->challenge = $challenge; 268 | $sigs[] = $sig; 269 | } 270 | return $sigs; 271 | } 272 | 273 | /** 274 | * Called to verify an authentication response 275 | * 276 | * @param array $requests An array of outstanding authentication requests 277 | * @param array $registrations An array of current registrations 278 | * @param object $response A response from the authenticator 279 | * @return Registration 280 | * @throws Error 281 | * 282 | * The Registration object returned on success contains an updated counter 283 | * that should be saved for future authentications. 284 | * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of 285 | * token cloning or similar and appropriate action should be taken. 286 | */ 287 | public function doAuthenticate(array $requests, array $registrations, $response) 288 | { 289 | if( !is_object( $response ) ) { 290 | throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.'); 291 | } 292 | 293 | if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) { 294 | throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING ); 295 | } 296 | 297 | /** @var object|null $req */ 298 | $req = null; 299 | 300 | /** @var object|null $reg */ 301 | $reg = null; 302 | 303 | $clientData = $this->base64u_decode($response->clientData); 304 | $decodedClient = json_decode($clientData); 305 | 306 | if(isset($decodedClient->typ) && $decodedClient->typ !== REQUEST_TYPE_AUTHENTICATE) { 307 | throw new Error('ClientData type is invalid', ERR_BAD_TYPE); 308 | } 309 | 310 | foreach ($requests as $req) { 311 | if( !is_object( $req ) ) { 312 | throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.'); 313 | } 314 | 315 | if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) { 316 | break; 317 | } 318 | 319 | $req = null; 320 | } 321 | if($req === null) { 322 | throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST ); 323 | } 324 | if(isset($decodedClient->origin) && !in_array($decodedClient->origin, $this->facetIds, true)) { 325 | throw new Error('App ID does not match the origin', ERR_NO_MATCHING_ORIGIN); 326 | } 327 | foreach ($registrations as $reg) { 328 | if( !is_object( $reg ) ) { 329 | throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.'); 330 | } 331 | 332 | if($reg->keyHandle === $response->keyHandle) { 333 | break; 334 | } 335 | $reg = null; 336 | } 337 | if($reg === null) { 338 | throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION ); 339 | } 340 | $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey)); 341 | if($pemKey === null) { 342 | throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE ); 343 | } 344 | 345 | $signData = $this->base64u_decode($response->signatureData); 346 | $dataToVerify = hash('sha256', $req->appId, true); 347 | $dataToVerify .= substr($signData, 0, 5); 348 | $dataToVerify .= hash('sha256', $clientData, true); 349 | $signature = substr($signData, 5); 350 | 351 | if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) { 352 | $upb = unpack("Cupb", substr($signData, 0, 1)); 353 | if($upb['upb'] !== 1) { 354 | throw new Error('User presence byte value is invalid', ERR_BAD_USER_PRESENCE ); 355 | } 356 | $ctr = unpack("Nctr", substr($signData, 1, 4)); 357 | $counter = $ctr['ctr']; 358 | /* TODO: wrap-around should be handled somehow.. */ 359 | if($counter > $reg->counter) { 360 | $reg->counter = $counter; 361 | return self::castObjectToRegistration($reg); 362 | } else { 363 | throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW ); 364 | } 365 | } else { 366 | throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE ); 367 | } 368 | } 369 | 370 | /** 371 | * @param object $object 372 | * @return Registration 373 | */ 374 | protected static function castObjectToRegistration($object) 375 | { 376 | $reg = new Registration(); 377 | if (isset($object->publicKey)) { 378 | $reg->publicKey = $object->publicKey; 379 | } 380 | if (isset($object->certificate)) { 381 | $reg->certificate = $object->certificate; 382 | } 383 | if (isset($object->counter)) { 384 | $reg->counter = $object->counter; 385 | } 386 | if (isset($object->keyHandle)) { 387 | $reg->keyHandle = $object->keyHandle; 388 | } 389 | return $reg; 390 | } 391 | 392 | /** 393 | * @return array 394 | */ 395 | private function get_certs() 396 | { 397 | $files = array(); 398 | $dir = $this->attestDir; 399 | if($dir !== null && is_dir($dir) && $handle = opendir($dir)) { 400 | while(false !== ($entry = readdir($handle))) { 401 | if(is_file("$dir/$entry")) { 402 | $files[] = "$dir/$entry"; 403 | } 404 | } 405 | closedir($handle); 406 | } elseif (is_file("$dir")) { 407 | $files[] = "$dir"; 408 | } 409 | return $files; 410 | } 411 | 412 | /** 413 | * @param string $data 414 | * @return string 415 | */ 416 | private function base64u_encode($data) 417 | { 418 | return trim(strtr(base64_encode($data), '+/', '-_'), '='); 419 | } 420 | 421 | /** 422 | * @param string $data 423 | * @return string 424 | */ 425 | private function base64u_decode($data) 426 | { 427 | return base64_decode(strtr($data, '-_', '+/')); 428 | } 429 | 430 | /** 431 | * @param string $key 432 | * @return null|string 433 | */ 434 | private function pubkey_to_pem($key) 435 | { 436 | if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") { 437 | return null; 438 | } 439 | 440 | /* 441 | * Convert the public key to binary DER format first 442 | * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 443 | * 444 | * SEQUENCE(2 elem) 30 59 445 | * SEQUENCE(2 elem) 30 13 446 | * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 447 | * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 448 | * BIT STRING(520 bit) 03 42 ..key.. 449 | */ 450 | $der = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01"; 451 | $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42"; 452 | $der .= "\0".$key; 453 | 454 | $pem = "-----BEGIN PUBLIC KEY-----\r\n"; 455 | $pem .= chunk_split(base64_encode($der), 64); 456 | $pem .= "-----END PUBLIC KEY-----"; 457 | 458 | return $pem; 459 | } 460 | 461 | /** 462 | * @return string 463 | * @throws Error 464 | */ 465 | private function createChallenge() 466 | { 467 | $challenge = random_bytes(32); 468 | $challenge = $this->base64u_encode( $challenge ); 469 | 470 | return $challenge; 471 | } 472 | 473 | /** 474 | * Fixes a certificate where the signature contains unused bits. 475 | * 476 | * @param string $cert 477 | * @return mixed 478 | */ 479 | private function fixSignatureUnusedBits($cert) 480 | { 481 | if(in_array(hash('sha256', $cert), $this->FIXCERTS, true)) { 482 | $cert[strlen($cert) - 257] = "\0"; 483 | } 484 | return $cert; 485 | } 486 | } 487 | 488 | /** 489 | * Class for building a registration request 490 | * 491 | * @package u2flib_server 492 | */ 493 | class RegisterRequest 494 | { 495 | /** @var string Protocol version */ 496 | public $version = U2F_VERSION; 497 | 498 | /** @var string Registration challenge */ 499 | public $challenge; 500 | 501 | /** @var string Application id */ 502 | public $appId; 503 | 504 | /** 505 | * @param string $challenge 506 | * @param string $appId 507 | * @internal 508 | */ 509 | public function __construct($challenge, $appId) 510 | { 511 | $this->challenge = $challenge; 512 | $this->appId = $appId; 513 | } 514 | } 515 | 516 | /** 517 | * Class for building up an authentication request 518 | * 519 | * @package u2flib_server 520 | */ 521 | class SignRequest 522 | { 523 | /** @var string Protocol version */ 524 | public $version = U2F_VERSION; 525 | 526 | /** @var string Authentication challenge */ 527 | public $challenge = ''; 528 | 529 | /** @var string Key handle of a registered authenticator */ 530 | public $keyHandle = ''; 531 | 532 | /** @var string Application id */ 533 | public $appId = ''; 534 | } 535 | 536 | /** 537 | * Class returned for successful registrations 538 | * 539 | * @package u2flib_server 540 | */ 541 | class Registration 542 | { 543 | /** @var string The key handle of the registered authenticator */ 544 | public $keyHandle = ''; 545 | 546 | /** @var string The public key of the registered authenticator */ 547 | public $publicKey = ''; 548 | 549 | /** @var string The attestation certificate of the registered authenticator */ 550 | public $certificate = ''; 551 | 552 | /** @var int The counter associated with this registration */ 553 | public $counter = -1; 554 | } 555 | 556 | /** 557 | * Error class, returned on errors 558 | * 559 | * @package u2flib_server 560 | */ 561 | class Error extends \Exception 562 | { 563 | /** 564 | * Override constructor and make message and code mandatory 565 | * @param string $message 566 | * @param int $code 567 | * @param \Exception|null $previous 568 | */ 569 | public function __construct($message, $code, \Exception $previous = null) { 570 | parent::__construct($message, $code, $previous); 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /tests/certs/yubico-u2f-ca-1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ 3 | dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw 4 | MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290 5 | IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 6 | AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk 7 | 5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep 8 | 8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw 9 | nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT 10 | 9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw 11 | LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ 12 | hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN 13 | BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4 14 | MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt 15 | hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k 16 | LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U 17 | sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc 18 | U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /tests/u2flib_test.php: -------------------------------------------------------------------------------- 1 | u2f = new u2flib_server\U2F("http://demo.example.com"); 39 | } 40 | 41 | public function testGetRegisterData() { 42 | list($reg, $signData) = $this->u2f->getRegisterData(); 43 | $this->assertJsonStringEqualsJsonString(json_encode(array()), json_encode($signData)); 44 | $this->assertEquals('U2F_V2', $reg->version); 45 | $this->assertObjectHasAttribute('challenge', $reg); 46 | $this->assertEquals('http://demo.example.com', $reg->appId); 47 | } 48 | 49 | public function testDoRegister() { 50 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 51 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9", "errorCode": 0 }'); 52 | $reg = $this->u2f->doRegister($req, $resp); 53 | $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $reg->keyHandle); 54 | $this->assertEquals('BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c=', $reg->publicKey); 55 | $this->assertEquals('MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp/VRZHOwd2NZNzpnB9ePNKvUaWCGK/gN+cynnYFdwJ75iSgMVYb/RnFcdPwnsBzBU68hbhTnu/FvJxWo7rZJ2q7qXpA10eLVXJr4/4oSXEk9I/0IIHqOP98Ck/fAoI5gYI7ygndyqoPJ/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh/h7oKEKamCWk19dJp5jHQmumkHlvQhH/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg/0J+xOb4zl6a1z65nae4OTj7628/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==', $reg->certificate); 56 | $this->assertLessThan(0, $reg->counter); 57 | } 58 | 59 | public function testDoRegisterNoCert() { 60 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 61 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 62 | $reg = $this->u2f->doRegister($req, $resp, false); 63 | $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $reg->keyHandle); 64 | $this->assertEquals('BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c=', $reg->publicKey); 65 | $this->assertEquals('', $reg->certificate); 66 | } 67 | 68 | /** 69 | * @expectedException u2flib_server\Error 70 | * @expectedExceptionCode u2flib_server\ERR_ATTESTATION_VERIFICATION 71 | */ 72 | public function testDoRegisterAttestFail() { 73 | $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs"); 74 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 75 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 76 | $this->u2f->doRegister($req, $resp, true); 77 | } 78 | 79 | /** 80 | * @expectedException u2flib_server\Error 81 | * @expectedExceptionCode u2flib_server\ERR_ATTESTATION_SIGNATURE 82 | */ 83 | public function testDoRegisterFail2() { 84 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 85 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8NwW=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 86 | $this->u2f->doRegister($req, $resp, false); 87 | } 88 | 89 | /** 90 | * @expectedException u2flib_server\Error 91 | * @expectedExceptionCode u2flib_server\ERR_UNMATCHED_CHALLENGE 92 | */ 93 | public function testDoRegisterFail() { 94 | $req = json_decode('{"version":"U2F_V2","challenge":"YKA0X075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 95 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 96 | $this->u2f->doRegister($req, $resp, false); 97 | } 98 | 99 | public function testDoRegisterAttest() { 100 | $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs"); 101 | $req = json_decode('{"version":"U2F_V2","challenge":"5CBRhGBb2CXSum71GNREBGft7yz9g1jZO7JTkHGFsVY","appId":"http:\/\/demo.example.com"}'); 102 | $resp = json_decode('{ "registrationData": "BQRX1gfcG-ofTlk9rjB9spsIMrmT9ba0DLto5fzk8FDB05ModNU2sWAqoQRemYiUrILQdbNGpN_aHA0_oq8kcd_XQCK-Ut0PWaOtz43t0aAV04U788e-dvpeqLtHxtINjgmutKM8_GJQ7F-3W0dogUjSANuRYRdkkSEHPcVdLSkpyfowggIbMIIBBaADAgECAgRAxBIlMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTA4NjU5MTUyNTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABK2iSVV7KGNEdPE-oHGvobNnHVw6ZZ6vB3jNIYB1C4t32OucHzMweHqM5CAMSMDHtfp1vuJYaiQSk7jb6M48WtejEjAQMA4GCisGAQQBgsQKAQEEADALBgkqhkiG9w0BAQsDggEBAVg0BoEHEEp4LJLYPYFACRGS8WZiXkCA8crYLgGnzvfKXwPwyKJlUzYxxv5xoRrl5zjkIUXhZ4mnHZVsnj9EY_VGDuRRzKX7YtxTZpFZn7ej3abjLhckTkkQ_AhUkmP7VuK2AWLgYsS8ejGUqughBsKvh_84uxTAEr5BS-OGg2yi7UIjd8W0nOCc6EN8d_8wCiPOjt2Y_-TKpLLTXKszk4UnWNzRdxBThmBBprJBZbF1VyVRvJm5yRLBpth3G8KMvrt4Nu3Ecoj_Q154IJpWe1Dp1upDFLOG9nWCRQk25Y264k9BDISfqs-wHvUjIo2iDnKl5UVoauTWaT7M6KuEwl4wRAIgYUVjS_yTwJAtF35glSbf9Et-5tJzlHOeAqmbACd6pwsCIE0MkTR5XNQoO4XqDaUZCXmadWu8yU1gfE7AJI9JUUcc", "clientData": "eyAiY2hhbGxlbmdlIjogIjVDQlJoR0JiMkNYU3VtNzFHTlJFQkdmdDd5ejlnMWpaTzdKVGtIR0ZzVlkiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 103 | $reg = $this->u2f->doRegister($req, $resp, true); 104 | $this->assertEquals('Ir5S3Q9Zo63Pje3RoBXThTvzx752-l6ou0fG0g2OCa60ozz8YlDsX7dbR2iBSNIA25FhF2SRIQc9xV0tKSnJ-g', $reg->keyHandle); 105 | $this->assertEquals('BFfWB9wb6h9OWT2uMH2ymwgyuZP1trQMu2jl/OTwUMHTkyh01TaxYCqhBF6ZiJSsgtB1s0ak39ocDT+iryRx39c=', $reg->publicKey); 106 | $this->assertEquals('MIICGzCCAQWgAwIBAgIEQMQSJTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDEwODY1OTE1MjUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAStoklVeyhjRHTxPqBxr6GzZx1cOmWerwd4zSGAdQuLd9jrnB8zMHh6jOQgDEjAx7X6db7iWGokEpO42+jOPFrXoxIwEDAOBgorBgEEAYLECgEBBAAwCwYJKoZIhvcNAQELA4IBAQBYNAaBBxBKeCyS2D2BQAkRkvFmYl5AgPHK2C4Bp873yl8D8MiiZVM2Mcb+caEa5ec45CFF4WeJpx2VbJ4/RGP1Rg7kUcyl+2LcU2aRWZ+3o92m4y4XJE5JEPwIVJJj+1bitgFi4GLEvHoxlKroIQbCr4f/OLsUwBK+QUvjhoNsou1CI3fFtJzgnOhDfHf/MAojzo7dmP/kyqSy01yrM5OFJ1jc0XcQU4ZgQaayQWWxdVclUbyZuckSwabYdxvCjL67eDbtxHKI/0NeeCCaVntQ6dbqQxSzhvZ1gkUJNuWNuuJPQQyEn6rPsB71IyKNog5ypeVFaGrk1mk+zOirhMJe', $reg->certificate); 107 | } 108 | 109 | public function testDoRegisterAttestSingleFile() { 110 | $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs/yubico-u2f-ca-1.pem"); 111 | $req = json_decode('{"version":"U2F_V2","challenge":"5CBRhGBb2CXSum71GNREBGft7yz9g1jZO7JTkHGFsVY","appId":"http:\/\/demo.example.com"}'); 112 | $resp = json_decode('{ "registrationData": "BQRX1gfcG-ofTlk9rjB9spsIMrmT9ba0DLto5fzk8FDB05ModNU2sWAqoQRemYiUrILQdbNGpN_aHA0_oq8kcd_XQCK-Ut0PWaOtz43t0aAV04U788e-dvpeqLtHxtINjgmutKM8_GJQ7F-3W0dogUjSANuRYRdkkSEHPcVdLSkpyfowggIbMIIBBaADAgECAgRAxBIlMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTA4NjU5MTUyNTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABK2iSVV7KGNEdPE-oHGvobNnHVw6ZZ6vB3jNIYB1C4t32OucHzMweHqM5CAMSMDHtfp1vuJYaiQSk7jb6M48WtejEjAQMA4GCisGAQQBgsQKAQEEADALBgkqhkiG9w0BAQsDggEBAVg0BoEHEEp4LJLYPYFACRGS8WZiXkCA8crYLgGnzvfKXwPwyKJlUzYxxv5xoRrl5zjkIUXhZ4mnHZVsnj9EY_VGDuRRzKX7YtxTZpFZn7ej3abjLhckTkkQ_AhUkmP7VuK2AWLgYsS8ejGUqughBsKvh_84uxTAEr5BS-OGg2yi7UIjd8W0nOCc6EN8d_8wCiPOjt2Y_-TKpLLTXKszk4UnWNzRdxBThmBBprJBZbF1VyVRvJm5yRLBpth3G8KMvrt4Nu3Ecoj_Q154IJpWe1Dp1upDFLOG9nWCRQk25Y264k9BDISfqs-wHvUjIo2iDnKl5UVoauTWaT7M6KuEwl4wRAIgYUVjS_yTwJAtF35glSbf9Et-5tJzlHOeAqmbACd6pwsCIE0MkTR5XNQoO4XqDaUZCXmadWu8yU1gfE7AJI9JUUcc", "clientData": "eyAiY2hhbGxlbmdlIjogIjVDQlJoR0JiMkNYU3VtNzFHTlJFQkdmdDd5ejlnMWpaTzdKVGtIR0ZzVlkiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 113 | $reg = $this->u2f->doRegister($req, $resp, true); 114 | $this->assertEquals('Ir5S3Q9Zo63Pje3RoBXThTvzx752-l6ou0fG0g2OCa60ozz8YlDsX7dbR2iBSNIA25FhF2SRIQc9xV0tKSnJ-g', $reg->keyHandle); 115 | $this->assertEquals('BFfWB9wb6h9OWT2uMH2ymwgyuZP1trQMu2jl/OTwUMHTkyh01TaxYCqhBF6ZiJSsgtB1s0ak39ocDT+iryRx39c=', $reg->publicKey); 116 | $this->assertEquals('MIICGzCCAQWgAwIBAgIEQMQSJTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDEwODY1OTE1MjUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAStoklVeyhjRHTxPqBxr6GzZx1cOmWerwd4zSGAdQuLd9jrnB8zMHh6jOQgDEjAx7X6db7iWGokEpO42+jOPFrXoxIwEDAOBgorBgEEAYLECgEBBAAwCwYJKoZIhvcNAQELA4IBAQBYNAaBBxBKeCyS2D2BQAkRkvFmYl5AgPHK2C4Bp873yl8D8MiiZVM2Mcb+caEa5ec45CFF4WeJpx2VbJ4/RGP1Rg7kUcyl+2LcU2aRWZ+3o92m4y4XJE5JEPwIVJJj+1bitgFi4GLEvHoxlKroIQbCr4f/OLsUwBK+QUvjhoNsou1CI3fFtJzgnOhDfHf/MAojzo7dmP/kyqSy01yrM5OFJ1jc0XcQU4ZgQaayQWWxdVclUbyZuckSwabYdxvCjL67eDbtxHKI/0NeeCCaVntQ6dbqQxSzhvZ1gkUJNuWNuuJPQQyEn6rPsB71IyKNog5ypeVFaGrk1mk+zOirhMJe', $reg->certificate); 117 | } 118 | 119 | /** 120 | * @expectedException u2flib_server\Error 121 | * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE 122 | */ 123 | public function testDoRegisterBadKeyInCert() { 124 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 125 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABdsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 126 | $this->u2f->doRegister($req, $resp); 127 | } 128 | 129 | /** 130 | * @expectedException u2flib_server\Error 131 | * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE 132 | */ 133 | public function testDoRegisterBadKey() { 134 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 135 | $resp = json_decode('{ "registrationData": "BQMtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 136 | $this->u2f->doRegister($req, $resp); 137 | } 138 | 139 | /** 140 | * @expectedException \InvalidArgumentException 141 | * @expectedExceptionMessage $request of doRegister() method only accepts object. 142 | */ 143 | public function testDoRegisterInvalidRequest() { 144 | $req = 'request'; 145 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 146 | $this->u2f->doRegister($req, $resp); 147 | } 148 | 149 | /** 150 | * @expectedException \InvalidArgumentException 151 | * @expectedExceptionMessage $response of doRegister() method only accepts object. 152 | */ 153 | public function testDoRegisterInvalidResponse() { 154 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 155 | $resp = 'response'; 156 | $this->u2f->doRegister($req, $resp); 157 | } 158 | 159 | /** 160 | * @expectedException u2flib_server\Error 161 | * @expectedExceptionCode u2flib_server\ERR_BAD_UA_RETURNING 162 | */ 163 | public function testDoRegisterUAError() { 164 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 165 | $resp = json_decode('{"errorCode": "4"}'); 166 | $this->u2f->doRegister($req, $resp); 167 | } 168 | 169 | /** 170 | * @expectedException \InvalidArgumentException 171 | * @expectedExceptionMessage $include_cert of doRegister() method only accepts boolean. 172 | */ 173 | public function testDoRegisterInvalidInclude_cert() { 174 | $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); 175 | $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); 176 | $this->u2f->doRegister($req, $resp, 'bar'); 177 | } 178 | 179 | public function testGetAuthenticateData() { 180 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); 181 | $data = $this->u2f->getAuthenticateData($regs); 182 | $inst = $data[0]; 183 | $this->assertEquals("U2F_V2", $inst->version); 184 | $this->assertObjectHasAttribute("challenge", $inst); 185 | $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $inst->keyHandle); 186 | $this->assertEquals('http://demo.example.com', $inst->appId); 187 | } 188 | 189 | /** 190 | * @expectedException \InvalidArgumentException 191 | * @expectedExceptionMessage $registrations of getAuthenticateData() method only accepts array of object. 192 | */ 193 | public function testGetAuthenticateDataInvalidRegistrations2() { 194 | $regs = array('YubiKey NEO', 'YubiKey Standard'); 195 | $data = $this->u2f->getAuthenticateData($regs); 196 | } 197 | 198 | public function testDoAuthenticate() { 199 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 200 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); 201 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w", "errorCode": 0 }'); 202 | $data = $this->u2f->doAuthenticate($reqs, $regs, $resp); 203 | $this->assertEquals(4, $data->counter); 204 | } 205 | 206 | /** 207 | * @expectedException u2flib_server\Error 208 | * @expectedExceptionCode u2flib_server\ERR_COUNTER_TOO_LOW 209 | */ 210 | public function testDoAuthenticateCtrFail() { 211 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 212 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":5}')); 213 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 214 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 215 | } 216 | 217 | /** 218 | * @expectedException u2flib_server\Error 219 | * @expectedExceptionCode u2flib_server\ERR_AUTHENTICATION_FAILURE 220 | */ 221 | public function testDoAuthenticateFail() { 222 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 223 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); 224 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAnG==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 225 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 226 | } 227 | 228 | /** 229 | * @expectedException u2flib_server\Error 230 | * @expectedExceptionCode u2flib_server\ERR_NO_MATCHING_REQUEST 231 | */ 232 | public function testDoAuthenticateWrongReq() { 233 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"cTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 234 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); 235 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 236 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 237 | } 238 | 239 | /** 240 | * @expectedException u2flib_server\Error 241 | * @expectedExceptionCode u2flib_server\ERR_NO_MATCHING_REGISTRATION 242 | */ 243 | public function testDoAuthenticateWrongReg() { 244 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 245 | $regs = array(json_decode('{"keyHandle":"cTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); 246 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 247 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 248 | } 249 | 250 | /** 251 | * @expectedException u2flib_server\Error 252 | * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE 253 | */ 254 | public function testDoAuthenticateBadKey() { 255 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 256 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"bC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); 257 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 258 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 259 | } 260 | 261 | /** 262 | * @expectedException \InvalidArgumentException 263 | * @expectedExceptionMessage $requests of doAuthenticate() method only accepts array of object. 264 | */ 265 | public function testDoAuthenticateInvalidRequests2() { 266 | $reqs = array('YubiKey NEO', 'YubiKey Standard'); 267 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); 268 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 269 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 270 | } 271 | 272 | /** 273 | * @expectedException \InvalidArgumentException 274 | * @expectedExceptionMessage $registrations of doAuthenticate() method only accepts array of object. 275 | */ 276 | public function testDoAuthenticateInvalidRegistrations2() { 277 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 278 | $regs = array('YubiKey NEO', 'YubiKey Standard'); 279 | $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); 280 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 281 | } 282 | 283 | /** 284 | * @expectedException \InvalidArgumentException 285 | * @expectedExceptionMessage $response of doAuthenticate() method only accepts object. 286 | */ 287 | public function testDoAuthenticateInvalidResponse() { 288 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 289 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); 290 | $resp = 'Response'; 291 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 292 | } 293 | 294 | /** 295 | * @expectedException u2flib_server\Error 296 | * @expectedExceptionCode u2flib_server\ERR_BAD_UA_RETURNING 297 | */ 298 | public function testDoAuthenticateUAError() { 299 | $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); 300 | $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); 301 | $resp = json_decode('{"errorCode": "5"}'); 302 | $this->u2f->doAuthenticate($reqs, $regs, $resp); 303 | } 304 | } 305 | 306 | ?> 307 | --------------------------------------------------------------------------------