diff --git a/.arcconfig b/.arcconfig
deleted file mode 100644
index ba871f4..0000000
--- a/.arcconfig
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "unit.engine": "Firehed\\Arctools\\Unit\\PHPUnitTestEngine",
- "load": [
- "vendor/firehed/arctools/src"
- ]
-}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3a1617e
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+/tests export-ignore
+/.editorconfig export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+
+*.php diff=php
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a839255
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..ce4db10
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,41 @@
+name: Lint
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ # Run on all PRs
+
+env:
+ CI: "true"
+
+jobs:
+ phpcs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v2
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer update
+ --no-ansi
+ --no-interaction
+ --no-progress
+ --no-suggest
+ --prefer-dist
+
+ - name: PHPCS
+ run: composer phpcs
diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
new file mode 100644
index 0000000..d1fd5c3
--- /dev/null
+++ b/.github/workflows/static-analysis.yml
@@ -0,0 +1,41 @@
+name: Static analysis
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ # Run on all PRs
+
+env:
+ CI: "true"
+
+jobs:
+ phpstan:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v2
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer update
+ --no-ansi
+ --no-interaction
+ --no-progress
+ --no-suggest
+ --prefer-dist
+
+ - name: PHPStan
+ run: composer phpstan
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..1ca349a
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,80 @@
+name: Test
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ # Run on all PRs
+
+env:
+ CI: "true"
+
+jobs:
+ phpunit:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ dependencies:
+ - 'high'
+ - 'low'
+ php:
+ - '7.2'
+ - '7.3'
+ - '7.4'
+ - '8.0'
+ - '8.1'
+ exclude:
+ - php: '8.1'
+ dependencies: 'low'
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ coverage: pcov
+ ini-values: zend.assertions=1, assert.exception=1, error_reporting=-1
+ php-version: ${{ matrix.php }}
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v2
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ matrix.dependencies }}-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}
+ restore-keys: |
+ ${{ runner.os }}-php-${{ matrix.dependencies }}-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}
+ ${{ runner.os }}-php-${{ matrix.dependencies }}-${{ matrix.php }}-
+ ${{ runner.os }}-php-${{ matrix.dependencies }}-
+ ${{ runner.os }}-php-
+
+ - name: Install highest dependencies
+ if: ${{ matrix.dependencies == 'high' }}
+ run: composer update
+ --no-ansi
+ --no-interaction
+ --no-progress
+ --no-suggest
+ --prefer-dist
+
+ - name: Install lowest dependencies
+ if: ${{ matrix.dependencies == 'low' }}
+ run: composer update
+ --no-ansi
+ --no-interaction
+ --no-progress
+ --no-suggest
+ --prefer-dist
+ --prefer-lowest
+
+ - name: PHPUnit
+ run: vendor/bin/phpunit
+ --coverage-clover coverage.xml
+
+ - name: Submit code coverage
+ if: ${{ always() }}
+ uses: codecov/codecov-action@v2
diff --git a/.gitignore b/.gitignore
index 38d8e6d..010fb9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
/examples/users/*
/examples/vendor/
/vendor/
+# Suggested for libraries
+composer.lock
+.phpunit.result.cache
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9bb78c4..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-language: php
-php:
- - '7.0'
-
-# From PHPUnit's config
-install:
- - travis_retry composer install --no-interaction --prefer-source
-
-script: php -d mbstring.func_overload=7 vendor/bin/phpunit --coverage-text --whitelist src/ tests/
diff --git a/README.md b/README.md
index 0d30fcc..781206d 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,11 @@
A PHP implementation of the FIDO U2F authentication standard
+[](https://github.com/Firehed/u2f-php/actions/workflows/lint.yml)
+[](https://github.com/Firehed/u2f-php/actions/workflows/static-analysis.yml)
+[](https://github.com/Firehed/u2f-php/actions/workflows/test.yml)
+[](https://codecov.io/gh/Firehed/u2f-php)
+
## Introduction
U2F, or Universal Second Factor, is a new authentication protocol designed "to augment the security of their existing password infrastructure by adding a strong second factor to user login"[1](https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-overview.html#background). It allows websites to replace the need for a companion app (such as Google Authenticator) with a single hardware token that will work across any website supporting the U2F protocol.
@@ -20,6 +25,13 @@ Additional resources:
* [FIDO U2F Overview](https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-overview.html)
* [FIDO U2F Javascript API](https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-javascript-api.html)
+## Installation
+
+`composer require firehed/u2f`
+
+Note: you **must not** be using the deprecated `mbstring.func_overload` functionality, which can completely break working on binary data.
+The library will immediately throw an exception if you have it enabled.
+
## Usage
Usage will be described in three parts: setup, registration, and authentication.
@@ -149,9 +161,7 @@ See its README for more information.
## Tests
-If you are using Arcanist, `arc unit` workflows will work as expected.
-
-Otherwise, all tests are in the `tests/` directory and can be run with `vendor/bin/phpunit tests/`.
+All tests are in the `tests/` directory and can be run with `vendor/bin/phpunit`.
## License
diff --git a/composer.json b/composer.json
index 7214806..4c0da2f 100644
--- a/composer.json
+++ b/composer.json
@@ -13,11 +13,13 @@
],
"homepage": "https://github.com/Firehed/u2f-php",
"require": {
- "php": ">=7.0"
+ "php": ">=7.2"
},
"require-dev": {
- "firehed/arctools": "^1",
- "phpunit/phpunit": "^5.2"
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^8.5",
+ "squizlabs/php_codesniffer": "^3.2",
+ "phpstan/phpstan-phpunit": "^0.12"
},
"autoload": {
"psr-4": {
@@ -37,5 +39,17 @@
"name": "Eric Stern",
"email": "eric@ericstern.com"
}
- ]
+ ],
+ "scripts": {
+ "test": [
+ "@phpunit",
+ "@phpstan",
+ "@phpcs"
+ ],
+ "coverage": "phpunit --coverage-html build; open build/index.html",
+ "autofix": "phpcbf src lib tests db",
+ "phpunit": "phpunit",
+ "phpstan": "phpstan analyse --no-progress",
+ "phpcs": "phpcs"
+ }
}
diff --git a/composer.lock b/composer.lock
deleted file mode 100644
index a821be3..0000000
--- a/composer.lock
+++ /dev/null
@@ -1,1150 +0,0 @@
-{
- "_readme": [
- "This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
- "This file is @generated automatically"
- ],
- "hash": "619e41093d2fdb704a016f75703d1297",
- "content-hash": "6206adc173e7b48e1071edb7819d70a8",
- "packages": [],
- "packages-dev": [
- {
- "name": "doctrine/instantiator",
- "version": "1.0.5",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/instantiator.git",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3,<8.0-DEV"
- },
- "require-dev": {
- "athletic/athletic": "~0.1.8",
- "ext-pdo": "*",
- "ext-phar": "*",
- "phpunit/phpunit": "~4.0",
- "squizlabs/php_codesniffer": "~2.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Marco Pivetta",
- "email": "ocramius@gmail.com",
- "homepage": "http://ocramius.github.com/"
- }
- ],
- "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://github.com/doctrine/instantiator",
- "keywords": [
- "constructor",
- "instantiate"
- ],
- "time": "2015-06-14 21:17:01"
- },
- {
- "name": "firehed/arctools",
- "version": "1.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/Firehed/arctools.git",
- "reference": "033cc82e777dd10a5416f544b37afb84877332ca"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/Firehed/arctools/zipball/033cc82e777dd10a5416f544b37afb84877332ca",
- "reference": "033cc82e777dd10a5416f544b37afb84877332ca",
- "shasum": ""
- },
- "require-dev": {
- "phpunit/phpunit": "~5.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Firehed\\Arctools\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Eric Stern",
- "email": "eric@ericstern.com"
- }
- ],
- "description": "Easily integrate with Arcanist and libphutil",
- "time": "2015-10-16 22:30:55"
- },
- {
- "name": "myclabs/deep-copy",
- "version": "1.5.0",
- "source": {
- "type": "git",
- "url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "e3abefcd7f106677fd352cd7c187d6c969aa9ddc"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e3abefcd7f106677fd352cd7c187d6c969aa9ddc",
- "reference": "e3abefcd7f106677fd352cd7c187d6c969aa9ddc",
- "shasum": ""
- },
- "require": {
- "php": ">=5.4.0"
- },
- "require-dev": {
- "doctrine/collections": "1.*",
- "phpunit/phpunit": "~4.1"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Create deep copies (clones) of your objects",
- "homepage": "https://github.com/myclabs/DeepCopy",
- "keywords": [
- "clone",
- "copy",
- "duplicate",
- "object",
- "object graph"
- ],
- "time": "2015-11-07 22:20:37"
- },
- {
- "name": "phpdocumentor/reflection-docblock",
- "version": "2.0.4",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
- "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.0"
- },
- "suggest": {
- "dflydev/markdown": "~1.0",
- "erusev/parsedown": "~1.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "phpDocumentor": [
- "src/"
- ]
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "mike.vanriel@naenius.com"
- }
- ],
- "time": "2015-02-03 12:10:50"
- },
- {
- "name": "phpspec/prophecy",
- "version": "v1.6.0",
- "source": {
- "type": "git",
- "url": "https://github.com/phpspec/prophecy.git",
- "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972",
- "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972",
- "shasum": ""
- },
- "require": {
- "doctrine/instantiator": "^1.0.2",
- "php": "^5.3|^7.0",
- "phpdocumentor/reflection-docblock": "~2.0",
- "sebastian/comparator": "~1.1",
- "sebastian/recursion-context": "~1.0"
- },
- "require-dev": {
- "phpspec/phpspec": "~2.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.5.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Prophecy\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Konstantin Kudryashov",
- "email": "ever.zet@gmail.com",
- "homepage": "http://everzet.com"
- },
- {
- "name": "Marcello Duarte",
- "email": "marcello.duarte@gmail.com"
- }
- ],
- "description": "Highly opinionated mocking framework for PHP 5.3+",
- "homepage": "https://github.com/phpspec/prophecy",
- "keywords": [
- "Double",
- "Dummy",
- "fake",
- "mock",
- "spy",
- "stub"
- ],
- "time": "2016-02-15 07:46:21"
- },
- {
- "name": "phpunit/php-code-coverage",
- "version": "3.3.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "fe33716763b604ade4cb442c0794f5bd5ad73004"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe33716763b604ade4cb442c0794f5bd5ad73004",
- "reference": "fe33716763b604ade4cb442c0794f5bd5ad73004",
- "shasum": ""
- },
- "require": {
- "php": "^5.6 || ^7.0",
- "phpunit/php-file-iterator": "~1.3",
- "phpunit/php-text-template": "~1.2",
- "phpunit/php-token-stream": "^1.4.2",
- "sebastian/code-unit-reverse-lookup": "~1.0",
- "sebastian/environment": "^1.3.2",
- "sebastian/version": "~1.0|~2.0"
- },
- "require-dev": {
- "ext-xdebug": ">=2.1.4",
- "phpunit/phpunit": "~5"
- },
- "suggest": {
- "ext-dom": "*",
- "ext-xdebug": ">=2.2.1",
- "ext-xmlwriter": "*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.3.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
- "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
- "keywords": [
- "coverage",
- "testing",
- "xunit"
- ],
- "time": "2016-03-03 08:49:08"
- },
- {
- "name": "phpunit/php-file-iterator",
- "version": "1.4.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
- "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "FilterIterator implementation that filters files based on a list of suffixes.",
- "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
- "keywords": [
- "filesystem",
- "iterator"
- ],
- "time": "2015-06-21 13:08:43"
- },
- {
- "name": "phpunit/php-text-template",
- "version": "1.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
- }
- ],
- "description": "Simple template engine.",
- "homepage": "https://github.com/sebastianbergmann/php-text-template/",
- "keywords": [
- "template"
- ],
- "time": "2015-06-21 13:50:34"
- },
- {
- "name": "phpunit/php-timer",
- "version": "1.0.7",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
- "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Utility class for timing",
- "homepage": "https://github.com/sebastianbergmann/php-timer/",
- "keywords": [
- "timer"
- ],
- "time": "2015-06-21 08:01:12"
- },
- {
- "name": "phpunit/php-token-stream",
- "version": "1.4.8",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
- "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da",
- "shasum": ""
- },
- "require": {
- "ext-tokenizer": "*",
- "php": ">=5.3.3"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.2"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Wrapper around PHP's tokenizer extension.",
- "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
- "keywords": [
- "tokenizer"
- ],
- "time": "2015-09-15 10:49:45"
- },
- {
- "name": "phpunit/phpunit",
- "version": "5.2.12",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "6f0948bab32270352f97d1913d82a49338dcb0da"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6f0948bab32270352f97d1913d82a49338dcb0da",
- "reference": "6f0948bab32270352f97d1913d82a49338dcb0da",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "ext-json": "*",
- "ext-pcre": "*",
- "ext-reflection": "*",
- "ext-spl": "*",
- "myclabs/deep-copy": "~1.3",
- "php": "^5.6 || ^7.0",
- "phpspec/prophecy": "^1.3.1",
- "phpunit/php-code-coverage": "^3.3.0",
- "phpunit/php-file-iterator": "~1.4",
- "phpunit/php-text-template": "~1.2",
- "phpunit/php-timer": ">=1.0.6",
- "phpunit/phpunit-mock-objects": ">=3.0.5",
- "sebastian/comparator": "~1.1",
- "sebastian/diff": "~1.2",
- "sebastian/environment": "~1.3",
- "sebastian/exporter": "~1.2",
- "sebastian/global-state": "~1.0",
- "sebastian/resource-operations": "~1.0",
- "sebastian/version": "~1.0|~2.0",
- "symfony/yaml": "~2.1|~3.0"
- },
- "suggest": {
- "phpunit/php-invoker": "~1.1"
- },
- "bin": [
- "phpunit"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.2.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
- }
- ],
- "description": "The PHP Unit Testing framework.",
- "homepage": "https://phpunit.de/",
- "keywords": [
- "phpunit",
- "testing",
- "xunit"
- ],
- "time": "2016-03-15 05:59:58"
- },
- {
- "name": "phpunit/phpunit-mock-objects",
- "version": "3.0.6",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "49bc700750196c04dd6bc2c4c99cb632b893836b"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/49bc700750196c04dd6bc2c4c99cb632b893836b",
- "reference": "49bc700750196c04dd6bc2c4c99cb632b893836b",
- "shasum": ""
- },
- "require": {
- "doctrine/instantiator": "^1.0.2",
- "php": ">=5.6",
- "phpunit/php-text-template": "~1.2",
- "sebastian/exporter": "~1.2"
- },
- "require-dev": {
- "phpunit/phpunit": "~5"
- },
- "suggest": {
- "ext-soap": "*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Mock Object library for PHPUnit",
- "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
- "keywords": [
- "mock",
- "xunit"
- ],
- "time": "2015-12-08 08:47:06"
- },
- {
- "name": "sebastian/code-unit-reverse-lookup",
- "version": "1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe",
- "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe",
- "shasum": ""
- },
- "require": {
- "php": ">=5.6"
- },
- "require-dev": {
- "phpunit/phpunit": "~5"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Looks up which function or method a line of code belongs to",
- "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
- "time": "2016-02-13 06:45:14"
- },
- {
- "name": "sebastian/comparator",
- "version": "1.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "937efb279bd37a375bcadf584dec0726f84dbf22"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22",
- "reference": "937efb279bd37a375bcadf584dec0726f84dbf22",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3",
- "sebastian/diff": "~1.2",
- "sebastian/exporter": "~1.2"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.4"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
- {
- "name": "Volker Dusch",
- "email": "github@wallbash.com"
- },
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Provides the functionality to compare PHP values for equality",
- "homepage": "http://www.github.com/sebastianbergmann/comparator",
- "keywords": [
- "comparator",
- "compare",
- "equality"
- ],
- "time": "2015-07-26 15:48:44"
- },
- {
- "name": "sebastian/diff",
- "version": "1.4.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e",
- "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.8"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Kore Nordmann",
- "email": "mail@kore-nordmann.de"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Diff implementation",
- "homepage": "https://github.com/sebastianbergmann/diff",
- "keywords": [
- "diff"
- ],
- "time": "2015-12-08 07:14:41"
- },
- {
- "name": "sebastian/environment",
- "version": "1.3.5",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
- "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.4"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.3.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Provides functionality to handle HHVM/PHP environments",
- "homepage": "http://www.github.com/sebastianbergmann/environment",
- "keywords": [
- "Xdebug",
- "environment",
- "hhvm"
- ],
- "time": "2016-02-26 18:40:46"
- },
- {
- "name": "sebastian/exporter",
- "version": "1.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "7ae5513327cb536431847bcc0c10edba2701064e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e",
- "reference": "7ae5513327cb536431847bcc0c10edba2701064e",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3",
- "sebastian/recursion-context": "~1.0"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.4"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
- {
- "name": "Volker Dusch",
- "email": "github@wallbash.com"
- },
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
- {
- "name": "Adam Harvey",
- "email": "aharvey@php.net"
- }
- ],
- "description": "Provides the functionality to export PHP variables for visualization",
- "homepage": "http://www.github.com/sebastianbergmann/exporter",
- "keywords": [
- "export",
- "exporter"
- ],
- "time": "2015-06-21 07:55:53"
- },
- {
- "name": "sebastian/global-state",
- "version": "1.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
- "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.2"
- },
- "suggest": {
- "ext-uopz": "*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Snapshotting of global state",
- "homepage": "http://www.github.com/sebastianbergmann/global-state",
- "keywords": [
- "global state"
- ],
- "time": "2015-10-12 03:26:01"
- },
- {
- "name": "sebastian/recursion-context",
- "version": "1.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "913401df809e99e4f47b27cdd781f4a258d58791"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791",
- "reference": "913401df809e99e4f47b27cdd781f4a258d58791",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.4"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
- {
- "name": "Adam Harvey",
- "email": "aharvey@php.net"
- }
- ],
- "description": "Provides functionality to recursively process PHP variables",
- "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2015-11-11 19:50:13"
- },
- {
- "name": "sebastian/resource-operations",
- "version": "1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
- "shasum": ""
- },
- "require": {
- "php": ">=5.6.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Provides a list of PHP built-in functions that operate on resources",
- "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2015-07-28 20:34:47"
- },
- {
- "name": "sebastian/version",
- "version": "2.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/version.git",
- "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5",
- "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5",
- "shasum": ""
- },
- "require": {
- "php": ">=5.6"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
- }
- ],
- "description": "Library that helps with managing the version number of Git-hosted PHP projects",
- "homepage": "https://github.com/sebastianbergmann/version",
- "time": "2016-02-04 12:56:52"
- },
- {
- "name": "symfony/yaml",
- "version": "v3.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/yaml.git",
- "reference": "b5ba64cd67ecd6887f63868fa781ca094bd1377c"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/b5ba64cd67ecd6887f63868fa781ca094bd1377c",
- "reference": "b5ba64cd67ecd6887f63868fa781ca094bd1377c",
- "shasum": ""
- },
- "require": {
- "php": ">=5.5.9"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.0-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Yaml\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Yaml Component",
- "homepage": "https://symfony.com",
- "time": "2016-02-23 15:16:06"
- }
- ],
- "aliases": [],
- "minimum-stability": "stable",
- "stability-flags": [],
- "prefer-stable": false,
- "prefer-lowest": false,
- "platform": {
- "php": ">=7.0"
- },
- "platform-dev": []
-}
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..3a54078
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,7 @@
+
+
+ src
+ tests
+
+
+
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..2c27d18
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,27 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Property Firehed\\\\U2F\\\\ClientData\\:\\:\\$cid_pubkey is never read, only written\\.$#"
+ count: 1
+ path: src/ClientData.php
+
+ -
+ message: "#^Property Firehed\\\\U2F\\\\ClientData\\:\\:\\$typ is never read, only written\\.$#"
+ count: 1
+ path: src/ClientData.php
+
+ -
+ message: "#^Method Firehed\\\\U2F\\\\FunctionsTest\\:\\:vectors\\(\\) should return array\\ but returns array\\(array\\('', ''\\), array\\('f', 'Zg'\\), array\\('fo', 'Zm8'\\), array\\('foo', 'Zm9v'\\), array\\('foob', 'Zm9vYg'\\), array\\('fooba', 'Zm9vYmE'\\), array\\('foobar', 'Zm9vYmFy'\\), array\\(string\\|false, 'AA_BB\\-cc'\\)\\)\\.$#"
+ count: 1
+ path: tests/FunctionsTest.php
+
+ -
+ message: "#^Call to an undefined static method object\\:\\:fromJson\\(\\)\\.$#"
+ count: 4
+ path: tests/ResponseTraitTest.php
+
+ -
+ message: "#^Method class@anonymous/tests/ResponseTraitTest\\.php\\:18\\:\\:parseResponse\\(\\) has parameter \\$response with no value type specified in iterable type array\\.$#"
+ count: 1
+ path: tests/ResponseTraitTest.php
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..71f50cc
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,9 @@
+includes:
+ - phpstan-baseline.neon
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+ - vendor/phpstan/phpstan/conf/bleedingEdge.neon
+parameters:
+ level: max
+ paths:
+ - src
+ - tests
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..6d0d482
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
diff --git a/src/AppIdTrait.php b/src/AppIdTrait.php
index 8a25558..ee38d01 100644
--- a/src/AppIdTrait.php
+++ b/src/AppIdTrait.php
@@ -5,22 +5,33 @@
trait AppIdTrait
{
+ /** @var string */
private $appId;
- public function getAppId(): string {
+ public function getAppId(): string
+ {
return $this->appId;
}
- public function setAppId(string $appId): self {
+ public function setAppId(string $appId): self
+ {
$this->appId = $appId;
return $this;
}
/**
- * @return the raw SHA-256 hash of the App ID
+ * @return string The raw SHA-256 hash of the App ID
*/
- public function getApplicationParameter(): string {
+ public function getApplicationParameter(): string
+ {
return hash('sha256', $this->appId, true);
}
+ /**
+ * @return string The raw SHA-256 hash of the Relying Party ID
+ */
+ public function getRpIdHash(): string
+ {
+ return hash('sha256', $this->appId, true);
+ }
}
diff --git a/src/AttestationCertificate.php b/src/AttestationCertificate.php
new file mode 100644
index 0000000..e0f8a29
--- /dev/null
+++ b/src/AttestationCertificate.php
@@ -0,0 +1,35 @@
+binary = $binary;
+ }
+
+ public function getBinary(): string
+ {
+ return $this->binary;
+ }
+
+ public function getPemFormatted(): string
+ {
+ $data = base64_encode($this->binary);
+ $pem = "-----BEGIN CERTIFICATE-----\r\n";
+ $pem .= chunk_split($data, 64);
+ $pem .= "-----END CERTIFICATE-----";
+ return $pem;
+ }
+
+ /** @return array{binary: string} */
+ public function __debugInfo(): array
+ {
+ return ['binary' => '0x' . bin2hex($this->binary)];
+ }
+}
diff --git a/src/AttestationCertificateInterface.php b/src/AttestationCertificateInterface.php
new file mode 100644
index 0000000..38f3f18
--- /dev/null
+++ b/src/AttestationCertificateInterface.php
@@ -0,0 +1,11 @@
+attest);
- }
-
- // PEM formatted cert
- public function getAttestationCertificatePem(): string {
- $pem = "-----BEGIN CERTIFICATE-----\r\n";
- $pem .= chunk_split($this->attest, 64);
- $pem .= "-----END CERTIFICATE-----";
- return $pem;
- }
-
- public function setAttestationCertificate(string $cert): self {
- // In the future, this may make assertions about the cert formatting;
- // right now, we're going to leave it be.
- $this->attest = base64_encode($cert);
- return $this;
- }
-
- public function verifyIssuerAgainstTrustedCAs(array $trusted_cas): bool {
- $result = openssl_x509_checkpurpose(
- $this->getAttestationCertificatePem(),
- \X509_PURPOSE_ANY,
- $trusted_cas);
- if ($result !== true) {
- throw new SecurityException(SecurityException::NO_TRUSTED_CA);
- }
- return $result;
- }
-
-}
diff --git a/src/ChallengeProvider.php b/src/ChallengeProvider.php
index 23df621..334b117 100644
--- a/src/ChallengeProvider.php
+++ b/src/ChallengeProvider.php
@@ -5,7 +5,5 @@
interface ChallengeProvider
{
-
public function getChallenge(): string;
-
}
diff --git a/src/ChallengeTrait.php b/src/ChallengeTrait.php
index a0bdb2d..767cb23 100644
--- a/src/ChallengeTrait.php
+++ b/src/ChallengeTrait.php
@@ -4,14 +4,17 @@
trait ChallengeTrait
{
+ /** @var string */
private $challenge;
// B64-websafe value (at no point is the binary version used)
- public function getChallenge(): string {
- return ($this->challenge);
+ public function getChallenge(): string
+ {
+ return $this->challenge;
}
- public function setChallenge(string $challenge): self {
+ public function setChallenge(string $challenge): self
+ {
// TODO: make immutable
$this->challenge = $challenge;
return $this;
diff --git a/src/ClientData.php b/src/ClientData.php
index 027269b..6034105 100644
--- a/src/ClientData.php
+++ b/src/ClientData.php
@@ -3,18 +3,26 @@
namespace Firehed\U2F;
-use JsonSerializable;
use Firehed\U2F\InvalidDataException as IDE;
-class ClientData implements JsonSerializable, ChallengeProvider
+class ClientData
{
use ChallengeTrait;
+ /** @var string */
+ private $originalJson;
+
+ /** @var string */
private $cid_pubkey;
+
+ /** @var string */
private $origin;
+
+ /** @var string */
private $typ;
- public static function fromJson(string $json) {
+ public static function fromJson(string $json): ClientData
+ {
$data = json_decode($json, true);
if (json_last_error() !== \JSON_ERROR_NONE) {
throw new IDE(IDE::MALFORMED_DATA, 'json');
@@ -23,24 +31,34 @@ public static function fromJson(string $json) {
$ret->setType($ret->validateKey('typ', $data));
$ret->setChallenge($ret->validateKey('challenge', $data));
$ret->origin = $ret->validateKey('origin', $data);
- $ret->cid_pubkey = $ret->validateKey('cid_pubkey', $data);
+ // This field is optional
+ if (isset($data['cid_pubkey'])) {
+ $ret->cid_pubkey = $data['cid_pubkey'];
+ }
+ $ret->originalJson = $json;
return $ret;
}
+ public function getApplicationParameter(): string
+ {
+ return hash('sha256', $this->origin, true);
+ }
+
/**
* Checks the 'typ' field against the allowed types in the U2F spec (sec.
* 7.1)
- * @param $type the 'typ' value
+ * @param string $type the 'typ' value
* @return $this
* @throws InvalidDataException if a non-conforming value is provided
*/
- private function setType(string $type): self {
+ private function setType(string $type): self
+ {
switch ($type) {
- case 'navigator.id.getAssertion': // fall through
- case 'navigator.id.finishEnrollment':
- break;
- default:
- throw new IDE(IDE::MALFORMED_DATA, 'typ');
+ case 'navigator.id.getAssertion': // fall through
+ case 'navigator.id.finishEnrollment':
+ break;
+ default:
+ throw new IDE(IDE::MALFORMED_DATA, 'typ');
}
$this->typ = $type;
return $this;
@@ -49,12 +67,13 @@ private function setType(string $type): self {
/**
* Checks for the presence of $key in $data. Returns the value if found,
* throws an InvalidDataException if missing
- * @param $key The array key to check
- * @param $data The array to check in
- * @return mixed The data, if present
+ * @param string $key The array key to check
+ * @param array $data The array to check in
+ * @return string The data, if present
* @throws InvalidDataException if not prsent
*/
- private function validateKey(string $key, array $data) {
+ private function validateKey(string $key, array $data): string
+ {
if (!array_key_exists($key, $data)) {
throw new IDE(IDE::MISSING_KEY, $key);
}
@@ -62,18 +81,8 @@ private function validateKey(string $key, array $data) {
}
// Returns the SHA256 hash of this object per the raw message formats spec
- public function getChallengeParameter(): string {
- $json = json_encode($this, \JSON_UNESCAPED_SLASHES);
- return hash('sha256', $json, true);
- }
-
- public function jsonSerialize() {
- return [
- 'typ' => $this->typ,
- 'challenge' => $this->getChallenge(),
- 'origin' => $this->origin,
- 'cid_pubkey' => $this->cid_pubkey,
- ];
+ public function getChallengeParameter(): string
+ {
+ return hash('sha256', $this->originalJson, true);
}
-
}
diff --git a/src/ClientErrorException.php b/src/ClientErrorException.php
index 9944045..a4f082c 100644
--- a/src/ClientErrorException.php
+++ b/src/ClientErrorException.php
@@ -2,11 +2,13 @@
declare(strict_types=1);
namespace Firehed\U2F;
+
use Exception;
class ClientErrorException extends Exception
{
- public function __construct(int $code) {
+ public function __construct(int $code)
+ {
parent::__construct(ClientError::MESSAGES[$code] ?? '', $code);
}
}
diff --git a/src/ECPublicKeyTrait.php b/src/ECPublicKey.php
similarity index 50%
rename from src/ECPublicKeyTrait.php
rename to src/ECPublicKey.php
index ee5f0a3..2ee1755 100644
--- a/src/ECPublicKeyTrait.php
+++ b/src/ECPublicKey.php
@@ -2,27 +2,46 @@
declare(strict_types=1);
namespace Firehed\U2F;
-use Firehed\U2F\InvalidDataException as IDE;
-trait ECPublicKeyTrait
+class ECPublicKey implements PublicKeyInterface
{
- // Stored base64-encoded
- private $pubKey = '';
+ /** @var string (binary) */
+ private $binary = '';
- // Binary string of public key
- public function getPublicKey(): string {
- return base64_decode($this->pubKey);
+ public function __construct(string $key)
+ {
+ // RFC5480 2.2 - must be uncompressed value
+ if ($key[0] !== "\x04") {
+ throw new InvalidDataException(
+ InvalidDataException::MALFORMED_DATA,
+ 'public key: first byte not x04 (uncompressed)'
+ );
+ }
+ if (strlen($key) !== 65) {
+ throw new InvalidDataException(
+ InvalidDataException::PUBLIC_KEY_LENGTH,
+ '65'
+ );
+ }
+ $this->binary = $key;
+ }
+
+ /**
+ * @return string The decoded public key.
+ */
+ public function getBinary(): string
+ {
+ return $this->binary;
}
// Prepends the pubkey format headers and builds a pem file from the raw
// public key component
- public function getPublicKeyPem(): string {
- $key = $this->getPublicKey();
-
+ public function getPemFormatted(): string
+ {
// Described in RFC 5480
// Just use an OID calculator to figure out *that* encoding
$der = hex2bin(
- '3059' // SEQUENCE, length 89
+ '3059' // SEQUENCE, length 89
.'3013' // SEQUENCE, length 19
.'0607' // OID, length 7
.'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
@@ -31,25 +50,20 @@ public function getPublicKeyPem(): string {
.'0342' // BIT STRING, length 66
.'00' // prepend with NUL - pubkey will follow
);
+ $der .= $this->binary;
- $der .= $key;
$pem = "-----BEGIN PUBLIC KEY-----\r\n";
$pem .= chunk_split(base64_encode($der), 64);
$pem .= "-----END PUBLIC KEY-----";
return $pem;
}
- public function setPublicKey(string $key): self {
- // RFC5480 2.2 - must be uncompressed value
- if ($key[0] !== "\x04") {
- throw new IDE(IDE::MALFORMED_DATA,
- 'public key: first byte not x04 (uncompressed)');
- }
- if (strlen($key) !== 65) {
- throw new IDE(IDE::PUBLIC_KEY_LENGTH, '65');
- }
- $this->pubKey = base64_encode($key);
- return $this;
+ /** @return array{x: string, y: string} */
+ public function __debugInfo(): array
+ {
+ return [
+ 'x' => '0x' . bin2hex(substr($this->binary, 1, 32)),
+ 'y' => '0x' . bin2hex(substr($this->binary, 33)),
+ ];
}
-
}
diff --git a/src/InvalidDataException.php b/src/InvalidDataException.php
index 948ca1d..24d3544 100644
--- a/src/InvalidDataException.php
+++ b/src/InvalidDataException.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
namespace Firehed\U2F;
+
use Exception;
class InvalidDataException extends Exception
@@ -16,11 +17,11 @@ class InvalidDataException extends Exception
self::PUBLIC_KEY_LENGTH => 'Public key length invalid, must be %s bytes',
];
- public function __construct(int $code, string ...$args) {
+ public function __construct(int $code, string ...$args)
+ {
$format = self::MESSAGES[$code] ?? 'Default message';
$message = sprintf($format, ...$args);
parent::__construct($message, $code);
}
-
}
diff --git a/src/KeyHandleInterface.php b/src/KeyHandleInterface.php
new file mode 100644
index 0000000..bef8c67
--- /dev/null
+++ b/src/KeyHandleInterface.php
@@ -0,0 +1,13 @@
+keyHandle);
+ public function getKeyHandleBinary(): string
+ {
+ return $this->keyHandle;
}
// B64-websafe value
- public function getKeyHandleWeb(): string {
+ public function getKeyHandleWeb(): string
+ {
return toBase64Web($this->getKeyHandleBinary());
}
// Binary value
- public function setKeyHandle(string $keyHandle): self {
+ public function setKeyHandle(string $keyHandle): self
+ {
// TODO: make immutable
- $this->keyHandle = base64_encode($keyHandle);
+ $this->keyHandle = $keyHandle;
return $this;
}
-
}
diff --git a/src/LoginResponseInterface.php b/src/LoginResponseInterface.php
new file mode 100644
index 0000000..f514cb3
--- /dev/null
+++ b/src/LoginResponseInterface.php
@@ -0,0 +1,15 @@
+ $this->version,
"challenge" => $this->getChallenge(),
diff --git a/src/RegisterResponse.php b/src/RegisterResponse.php
index 4fda0d3..c417008 100644
--- a/src/RegisterResponse.php
+++ b/src/RegisterResponse.php
@@ -2,15 +2,30 @@
declare(strict_types=1);
namespace Firehed\U2F;
+
use Firehed\U2F\InvalidDataException as IDE;
-class RegisterResponse
+class RegisterResponse implements RegistrationResponseInterface
{
- use AttestationCertificateTrait;
- use ECPublicKeyTrait;
use ResponseTrait;
- protected function parseResponse(array $response): self {
+ /** @var AttestationCertificate */
+ private $cert;
+
+ /** @var PublicKeyInterface */
+ private $pubKey;
+
+ /**
+ * @param array{
+ * registrationData: string,
+ * version: string,
+ * chalenge: string,
+ * appId: string,
+ * clientData: string,
+ * } $response
+ */
+ protected function parseResponse(array $response): self
+ {
$this->validateKeyInArray('registrationData', $response);
// Binary string as defined by
// U2F 1.0 Raw Message Format Sec. 4.3
@@ -19,20 +34,24 @@ protected function parseResponse(array $response): self {
// Basic fixed length check
if (strlen($regData) < 67) {
- throw new IDE(IDE::MALFORMED_DATA,
- 'registrationData is missing information');
+ throw new IDE(
+ IDE::MALFORMED_DATA,
+ 'registrationData is missing information'
+ );
}
$offset = 0; // Number of bytes read so far (think fread/fseek)
$reserved = ord($regData[$offset]);
if ($reserved !== 5) {
- throw new IDE(IDE::MALFORMED_DATA,
- 'reserved byte');
+ throw new IDE(
+ IDE::MALFORMED_DATA,
+ 'reserved byte'
+ );
}
$offset += 1;
- $this->setPublicKey(substr($regData, $offset, 65));
+ $this->pubKey = new ECPublicKey(substr($regData, $offset, 65));
$offset += 65;
$keyHandleLength = ord($regData[$offset]);
@@ -40,8 +59,10 @@ protected function parseResponse(array $response): self {
// Dynamic length check through key handle
if (strlen($regData) < $offset+$keyHandleLength) {
- throw new IDE(IDE::MALFORMED_DATA,
- 'key handle length');
+ throw new IDE(
+ IDE::MALFORMED_DATA,
+ 'key handle length'
+ );
}
$this->setKeyHandle(substr($regData, $offset, $keyHandleLength));
$offset += $keyHandleLength;
@@ -59,16 +80,20 @@ protected function parseResponse(array $response): self {
$remain = substr($regData, $offset);
$b0 = ord($remain[0]);
if (($b0 & 0x1F) != 0x10) {
- throw new IDE(IDE::MALFORMED_DATA,
- 'starting byte of attestation certificate');
+ throw new IDE(
+ IDE::MALFORMED_DATA,
+ 'starting byte of attestation certificate'
+ );
}
$length = ord($remain[1]);
if (($length & 0x80) == 0x80) {
$needed = $length ^ 0x80;
if ($needed > 4) {
// This would be a >4GB cert, reject it out of hand
- throw new IDE(IDE::MALFORMED_DATA,
- 'certificate length');
+ throw new IDE(
+ IDE::MALFORMED_DATA,
+ 'certificate length'
+ );
}
$bytes = 0;
// Start 2 bytes in, for SEQUENCE and its LENGTH
@@ -83,10 +108,13 @@ protected function parseResponse(array $response): self {
// data, in case a malformed cert was provided to trigger an overflow
// during parsing
if ($length + $offset > strlen($regData)) {
- throw new IDE(IDE::MALFORMED_DATA,
- 'certificate and sigature length');
+ throw new IDE(
+ IDE::MALFORMED_DATA,
+ 'certificate and sigature length'
+ );
}
- $this->setAttestationCertificate(substr($regData, $offset, $length));
+ $cert = new AttestationCertificate(substr($regData, $offset, $length));
+ $this->cert = $cert;
$offset += $length;
// All remaining data is the signature
@@ -95,4 +123,36 @@ protected function parseResponse(array $response): self {
return $this;
}
+ public function getSignedData(): string
+ {
+ // https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#fig-authentication-request-message
+ return sprintf(
+ '%s%s%s%s%s',
+ chr(0),
+ $this->clientData->getApplicationParameter(),
+ $this->clientData->getChallengeParameter(),
+ $this->getKeyHandleBinary(),
+ $this->pubKey->getBinary()
+ );
+ }
+
+ public function getRpIdHash(): string
+ {
+ return $this->clientData->getApplicationParameter();
+ }
+
+ public function getAttestationCertificate(): AttestationCertificateInterface
+ {
+ return $this->cert;
+ }
+
+ public function getChallenge(): string
+ {
+ return $this->clientData->getChallenge();
+ }
+
+ public function getPublicKey(): PublicKeyInterface
+ {
+ return $this->pubKey;
+ }
}
diff --git a/src/Registration.php b/src/Registration.php
index 546c6d1..4eaca59 100644
--- a/src/Registration.php
+++ b/src/Registration.php
@@ -2,20 +2,40 @@
declare(strict_types=1);
namespace Firehed\U2F;
+
use OutOfBoundsException;
-class Registration
+class Registration implements RegistrationInterface
{
- use AttestationCertificateTrait;
- use ECPublicKeyTrait;
use KeyHandleTrait;
+ /** @var AttestationCertificateInterface */
+ private $cert;
+
+ /** @var int */
private $counter = -1;
- public function getCounter(): int {
+ /** @var PublicKeyInterface */
+ private $publicKey;
+
+ public function getAttestationCertificate(): AttestationCertificateInterface
+ {
+ return $this->cert;
+ }
+
+ public function getCounter(): int
+ {
return $this->counter;
}
- public function setCounter(int $counter): self {
+
+ public function setAttestationCertificate(AttestationCertificateInterface $cert): self
+ {
+ $this->cert = $cert;
+ return $this;
+ }
+
+ public function setCounter(int $counter): self
+ {
if ($counter < 0) {
throw new OutOfBoundsException('Counter may not be negative');
}
@@ -23,4 +43,36 @@ public function setCounter(int $counter): self {
return $this;
}
+ public function getPublicKey(): PublicKeyInterface
+ {
+ return $this->publicKey;
+ }
+
+ public function setPublicKey(PublicKeyInterface $publicKey): self
+ {
+ $this->publicKey = $publicKey;
+ return $this;
+ }
+
+ /**
+ * @return array{
+ * cert: AttestationCertificateInterface,
+ * counter: int,
+ * publicKey: PublicKeyInterface,
+ * keyHandle: string,
+ * }
+ */
+ public function __debugInfo(): array
+ {
+ $hex = function (string $binary): string {
+ return '0x' . bin2hex($binary);
+ };
+
+ return [
+ 'cert' => $this->cert,
+ 'counter' => $this->counter,
+ 'publicKey' => $this->publicKey,
+ 'keyHandle' => $hex($this->keyHandle),
+ ];
+ }
}
diff --git a/src/RegistrationInterface.php b/src/RegistrationInterface.php
new file mode 100644
index 0000000..df3557d
--- /dev/null
+++ b/src/RegistrationInterface.php
@@ -0,0 +1,26 @@
+signature);
- }
+ /** @var string (binary) */
+ private $signature = '';
- public function getClientData(): ClientData {
- return $this->clientData;
+ public function getSignature(): string
+ {
+ return $this->signature;
}
- protected function setSignature(string $signature): self {
- $this->signature = base64_encode($signature);
+ protected function setSignature(string $signature): self
+ {
+ $this->signature = $signature;
return $this;
}
- public static function fromJson(string $json): self {
+ public static function fromJson(string $json): self
+ {
$data = json_decode($json, true);
if (json_last_error() !== \JSON_ERROR_NONE) {
throw new IDE(IDE::MALFORMED_DATA, 'JSON');
@@ -44,7 +44,9 @@ public static function fromJson(string $json): self {
abstract protected function parseResponse(array $response): self;
- private function validateKeyInArray(string $key, array $data): bool {
+ /** @param array $data */
+ private function validateKeyInArray(string $key, array $data): bool
+ {
if (!isset($data[$key])) {
throw new IDE(IDE::MISSING_KEY, $key);
}
@@ -53,5 +55,4 @@ private function validateKeyInArray(string $key, array $data): bool {
}
return true;
}
-
}
diff --git a/src/SecurityException.php b/src/SecurityException.php
index 01f9ed1..2d2ed73 100644
--- a/src/SecurityException.php
+++ b/src/SecurityException.php
@@ -2,6 +2,7 @@
declare(strict_types=1);
namespace Firehed\U2F;
+
use Exception;
class SecurityException extends Exception
@@ -11,17 +12,23 @@ class SecurityException extends Exception
const CHALLENGE_MISMATCH = 3;
const KEY_HANDLE_UNRECOGNIZED = 4;
const NO_TRUSTED_CA = 5;
+ const WRONG_RELYING_PARTY = 6;
const MESSAGES = [
self::SIGNATURE_INVALID => 'Signature verification failed',
- self::COUNTER_USED => 'Response counter value is too low, indicating a possible replay attack or cloned token. It is also possible but unlikely that the token\'s internal counter wrapped around. This token should be invalidated or flagged for review.',
+ self::COUNTER_USED =>
+ 'Response counter value is too low, indicating a possible replay '.
+ 'attack or cloned token. It is also possible but unlikely that '.
+ 'the token\'s internal counter wrapped around. This token should '.
+ 'be invalidated or flagged for review.',
self::CHALLENGE_MISMATCH => 'Response challenge does not match request',
self::KEY_HANDLE_UNRECOGNIZED => 'Key handle has not been registered',
self::NO_TRUSTED_CA => 'The attestation certificate was not signed by any trusted Certificate Authority',
+ self::WRONG_RELYING_PARTY => 'Relying party invalid for this server',
];
- public function __construct(int $code) {
+ public function __construct(int $code)
+ {
parent::__construct(self::MESSAGES[$code] ?? '', $code);
}
-
}
diff --git a/src/Server.php b/src/Server.php
index d3e892e..a3274df 100644
--- a/src/Server.php
+++ b/src/Server.php
@@ -5,77 +5,104 @@
use BadMethodCallException;
use Firehed\U2F\SecurityException as SE;
+use RuntimeException;
class Server
{
-
use AppIdTrait;
/**
* Holds a list of paths to PEM-formatted CA certificates. Unless
* verification has been explicitly disabled with `disableCAVerification()`,
- * the Attestation Certificate in the `RegisterResponse` will be validated
- * against the provided CAs.
+ * the Attestation Certificate in the `RegistrationResponseInterface` will
+ * be validated against the provided CAs.
*
* This means that you *must* either a) provide a list of trusted
* certificates, or b) explicitly disable verifiation. By default, it will
* attempt to validate against an empty list which will always fail. This
* is by design.
+ *
+ * @var string[]
*/
private $trustedCAs = [];
/**
* Indicates whether to verify against `$trustedCAs`. Must be explicitly
* disabled with `disableCAVerification()`.
+ *
+ * @var bool
*/
private $verifyCA = true;
/**
* Holds a RegisterRequest used by `register()`, which contains the
* challenge in the signature.
+ *
+ * @var ?RegisterRequest
*/
private $registerRequest;
/**
* Holds Registrations that were previously established by the
* authenticating party during `authenticate()`
+ *
+ * @var RegistrationInterface[]
*/
private $registrations = [];
/**
* Holds SignRequests used by `authenticate` which contain the challenge
* that's part of the signed response.
+ *
+ * @var SignRequest[]
*/
private $signRequests = [];
+ public function __construct()
+ {
+ $overload = ini_get('mbstring.func_overload');
+ // @codeCoverageIgnoreStart
+ if ($overload > 0) {
+ throw new RuntimeException(
+ 'The deprecated "mbstring.func_overload" directive must be disabled'
+ );
+ }
+ // @codeCoverageIgnoreEnd
+ }
/**
- * This method authenticates a `SignResponse` against outstanding
- * `Registrations` and their corresponding `SignRequest`s. If the
- * response's signature validates and the counter hasn't done anything
- * strange, the Registration will be returned with an updated counter
- * value, which *must* be persisted for the next authentication. If any
- * verification component fails, a `SE` will be thrown.
+ * This method authenticates a `LoginResponseInterface` against outstanding
+ * registrations and their corresponding `SignRequest`s. If the response's
+ * signature validates and the counter hasn't done anything strange, the
+ * registration will be returned with an updated counter value, which *must*
+ * be persisted for the next authentication. If any verification component
+ * fails, a `SE` will be thrown.
*
- * @param SignResponse the parsed response from the user
- * @return Registration if authentication succeeds
+ * @param LoginResponseInterface $response the parsed response from the user
+ * @return RegistrationInterface if authentication succeeds
* @throws SE if authentication fails
* @throws BadMethodCallException if a precondition is not met
*/
- public function authenticate(SignResponse $response): Registration {
+ public function authenticate(LoginResponseInterface $response): RegistrationInterface
+ {
if (!$this->registrations) {
throw new BadMethodCallException(
- 'Before calling authenticate(), provide `Registration`s with '.
- 'setRegistrations()');
+ 'Before calling authenticate(), provide objects implementing'.
+ 'RegistrationInterface with setRegistrations()'
+ );
}
if (!$this->signRequests) {
throw new BadMethodCallException(
'Before calling authenticate(), provide `SignRequest`s with '.
- 'setSignRequests()');
+ 'setSignRequests()'
+ );
}
// Search for the registration to use based on the Key Handle
- $registration = $this->findObjectWithKeyHandle($this->registrations,
- $response->getKeyHandleBinary());
+ /** @var ?Registration */
+ $registration = $this->findObjectWithKeyHandle(
+ $this->registrations,
+ $response->getKeyHandleBinary()
+ );
if (!$registration) {
// This would suggest either some sort of forgery attempt or
// a hilariously-broken token responding to handles it doesn't
@@ -84,8 +111,10 @@ public function authenticate(SignResponse $response): Registration {
}
// Search for the Signing Request to use based on the Key Handle
- $request = $this->findObjectWithKeyHandle($this->signRequests,
- $registration->getKeyHandleBinary());
+ $request = $this->findObjectWithKeyHandle(
+ $this->signRequests,
+ $registration->getKeyHandleBinary()
+ );
if (!$request) {
// Similar to above, there is a bizarre mismatch between the known
// possible sign requests and the key handle determined above. This
@@ -98,27 +127,15 @@ public function authenticate(SignResponse $response): Registration {
// match the one in the signing request, the client signed the
// wrong thing. This could possibly be an attempt at a replay
// attack.
- $this->validateChallenge($response->getClientData(), $request);
-
- $pem = $registration->getPublicKeyPem();
-
- // U2F Spec:
- // https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success
- $to_verify = sprintf('%s%s%s%s',
- $request->getApplicationParameter(),
- chr($response->getUserPresenceByte()),
- pack('N', $response->getCounter()),
- // Note: Spec says this should be from the request, but that's not
- // actually available via the JS API. Because we assert the
- // challenge *value* from the Client Data matches the trusted one
- // from the SignRequest and that value is included in the Challenge
- // Parameter, this is safe unless/until SHA-256 is broken.
- $response->getClientData()->getChallengeParameter()
- );
+ $this->validateChallenge($request, $response);
+
+ $pem = $registration->getPublicKey()->getPemFormatted();
+
+ $toVerify = $response->getSignedData();
// Signature must validate against
$sig_check = openssl_verify(
- $to_verify,
+ $toVerify,
$response->getSignature(),
$pem,
\OPENSSL_ALGO_SHA256
@@ -162,51 +179,44 @@ public function authenticate(SignResponse $response): Registration {
// again. There's no perfect way to handle this since
return (new Registration())
- ->setAttestationCertificate($registration->getAttestationCertificateBinary())
+ ->setAttestationCertificate($registration->getAttestationCertificate())
->setKeyHandle($registration->getKeyHandleBinary())
->setPublicKey($registration->getPublicKey())
->setCounter($response->getCounter());
- return true;
}
/**
- * This method authenticates a RegisterResponse against its corresponding
- * RegisterRequest by verifying the certificate and signature. If valid, it
- * returns a Registration object; if not, a SE will be
- * thrown and attempt to register the key must be aborted.
+ * This method authenticates a RegistrationResponseInterface against its
+ * corresponding RegisterRequest by verifying the certificate and signature.
+ * If valid, it returns a registration; if not, a SE will be thrown and
+ * attempt to register the key must be aborted.
*
- * @param The response to verify
- * @return Registration if the response is proven authentic
+ * @param RegistrationResponseInterface $response The response to verify
+ * @return RegistrationInterface if the response is proven authentic
* @throws SE if the response cannot be proven authentic
* @throws BadMethodCallException if a precondition is not met
*/
- public function register(RegisterResponse $resp): Registration {
+ public function register(RegistrationResponseInterface $response): RegistrationInterface
+ {
if (!$this->registerRequest) {
throw new BadMethodCallException(
'Before calling register(), provide a RegisterRequest '.
- 'with setRegisterRequest()');
+ 'with setRegisterRequest()'
+ );
}
- $this->validateChallenge($resp->getClientData(), $this->registerRequest);
- // Check the Application Parameter?
-
- // https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#registration-response-message-success
- $signed_data = sprintf('%s%s%s%s%s',
- chr(0),
- $this->registerRequest->getApplicationParameter(),
- $resp->getClientData()->getChallengeParameter(),
- $resp->getKeyHandleBinary(),
- $resp->getPublicKey()
- );
+ $this->validateChallenge($this->registerRequest, $response);
+ // Check the Application Parameter
+ $this->validateRelyingParty($response->getRpIdHash());
- $pem = $resp->getAttestationCertificatePem();
if ($this->verifyCA) {
- $resp->verifyIssuerAgainstTrustedCAs($this->trustedCAs);
+ $this->verifyAttestationCertAgainstTrustedCAs($response);
}
// Signature must validate against device issuer's public key
+ $pem = $response->getAttestationCertificate()->getPemFormatted();
$sig_check = openssl_verify(
- $signed_data,
- $resp->getSignature(),
+ $response->getSignedData(),
+ $response->getSignature(),
$pem,
\OPENSSL_ALGO_SHA256
);
@@ -215,10 +225,10 @@ public function register(RegisterResponse $resp): Registration {
}
return (new Registration())
- ->setAttestationCertificate($resp->getAttestationCertificateBinary())
+ ->setAttestationCertificate($response->getAttestationCertificate())
->setCounter(0) // The response does not include this
- ->setKeyHandle($resp->getKeyHandleBinary())
- ->setPublicKey($resp->getPublicKey());
+ ->setKeyHandle($response->getKeyHandleBinary())
+ ->setPublicKey($response->getPublicKey());
}
/**
@@ -231,7 +241,8 @@ public function register(RegisterResponse $resp): Registration {
*
* @return self
*/
- public function disableCAVerification(): self {
+ public function disableCAVerification(): self
+ {
$this->verifyCA = false;
return $this;
}
@@ -243,10 +254,11 @@ public function disableCAVerification(): self {
* This method or disableCAVerification must be called before register() or
* a SecurityException will always be thrown.
*
- * @param array A list of file paths to device issuer CA certs
+ * @param string[] $CAs A list of file paths to device issuer CA certs
* @return self
*/
- public function setTrustedCAs(array $CAs): self {
+ public function setTrustedCAs(array $CAs): self
+ {
$this->verifyCA = true;
$this->trustedCAs = $CAs;
return $this;
@@ -256,22 +268,26 @@ public function setTrustedCAs(array $CAs): self {
* Provide the previously-generated RegisterRequest to be used when
* verifying a RegisterResponse during register()
*
- * @param RegisterRequest
+ * @param RegisterRequest $request
* @return self
*/
- public function setRegisterRequest(RegisterRequest $request): self {
+ public function setRegisterRequest(RegisterRequest $request): self
+ {
$this->registerRequest = $request;
return $this;
}
/**
- * Provide a user's existing Registrations to be used during authentication
+ * Provide a user's existing registration to be used during
+ * authentication
*
- * @param array
+ * @param RegistrationInterface[] $registrations
* @return self
*/
- public function setRegistrations(array $registrations): self {
- array_map(function(Registration $r){}, $registrations); // type check
+ public function setRegistrations(array $registrations): self
+ {
+ array_map(function (RegistrationInterface $r) {
+ }, $registrations); // type check
$this->registrations = $registrations;
return $this;
}
@@ -281,11 +297,13 @@ public function setRegistrations(array $registrations): self {
* existing Registrations, of of which should be signed and will be
* verified during authenticate()
*
- * @param array
+ * @param SignRequest[] $signRequests
* @return self
*/
- public function setSignRequests(array $signRequests): self {
- array_map(function(SignRequest $s){}, $signRequests); // type check
+ public function setSignRequests(array $signRequests): self
+ {
+ array_map(function (SignRequest $s) {
+ }, $signRequests); // type check
$this->signRequests = $signRequests;
return $this;
}
@@ -296,20 +314,22 @@ public function setSignRequests(array $signRequests): self {
*
* @return RegisterRequest
*/
- public function generateRegisterRequest(): RegisterRequest {
+ public function generateRegisterRequest(): RegisterRequest
+ {
return (new RegisterRequest())
->setAppId($this->getAppId())
->setChallenge($this->generateChallenge());
}
/**
- * Creates a new SignRequest for an existing Registration for an
+ * Creates a new SignRequest for an existing registration for an
* authenticating user, used by the `u2f.sign` API.
*
- * @param Registration one of the user's existing Registrations
+ * @param RegistrationInterface $reg one of the user's existing Registrations
* @return SignRequest
*/
- public function generateSignRequest(Registration $reg): SignRequest {
+ public function generateSignRequest(RegistrationInterface $reg): SignRequest
+ {
return (new SignRequest())
->setAppId($this->getAppId())
->setChallenge($this->generateChallenge())
@@ -317,12 +337,13 @@ public function generateSignRequest(Registration $reg): SignRequest {
}
/**
- * Wraps generateSignRequest for multiple Registrations
+ * Wraps generateSignRequest for multiple registrations
*
- * @param array
- * @return array
+ * @param RegistrationInterface[] $registrations
+ * @return SignRequest[]
*/
- public function generateSignRequests(array $registrations): array {
+ public function generateSignRequests(array $registrations): array
+ {
return array_values(array_map([$this, 'generateSignRequest'], $registrations));
}
@@ -331,10 +352,12 @@ public function generateSignRequests(array $registrations): array {
* key handle value. If one is found, it is returned; if not, this returns
* null.
*
- * @param array<> haystack to search
- * @param string key handle to find in haystack
- * @return mixed element from haystack
- * @return null if no element matches
+ * @template T of KeyHandleInterface
+ *
+ * @param T[] $objects haystack to search
+ * @param string $keyHandle key handle to find in haystack
+ *
+ * @return ?T element from haystack if match found, otherwise null
*/
private function findObjectWithKeyHandle(
array $objects,
@@ -353,25 +376,32 @@ private function findObjectWithKeyHandle(
*
* @return string
*/
- private function generateChallenge(): string {
+ private function generateChallenge(): string
+ {
// FIDO Alliance spec suggests a minimum of 8 random bytes
return toBase64Web(\random_bytes(16));
}
+ private function validateRelyingParty(string $rpIdHash): void
+ {
+ // Note: this is a bit delicate at the moment, since different
+ // protocols have different rules around the handling of Relying Party
+ // verification. Expect this to be revised.
+ if (!hash_equals($this->getRpIdHash(), $rpIdHash)) {
+ throw new SE(SE::WRONG_RELYING_PARTY);
+ }
+ }
/**
* Compares the Challenge value from a known source against the
* user-provided value. A mismatch will throw a SE. Future
* versions may also enforce a timing window.
*
- * @param ChallengeProvider source of known challenge
- * @param ChallengeProvider user-provided value
- * @return true on success
+ * @param ChallengeProvider $from source of known challenge
+ * @param ChallengeProvider $to user-provided value
* @throws SE on failure
*/
- private function validateChallenge(
- ChallengeProvider $from,
- ChallengeProvider $to
- ): bool {
+ private function validateChallenge(ChallengeProvider $from, ChallengeProvider $to): void
+ {
// Note: strictly speaking, this shouldn't even be targetable as
// a timing attack. However, this opts to be proactive, and also
// ensures that no weird PHP-isms in string handling cause mismatched
@@ -379,7 +409,27 @@ private function validateChallenge(
if (!hash_equals($from->getChallenge(), $to->getChallenge())) {
throw new SE(SE::CHALLENGE_MISMATCH);
}
- // TOOD: generate and compare timestamps
- return true;
+ }
+
+ /**
+ * Asserts that the attestation cert provided by the registration is issued
+ * by the set of trusted CAs.
+ *
+ * @param RegistrationResponseInterface $response The response to validate
+ * @throws SecurityException upon failure
+ * @return void
+ */
+ private function verifyAttestationCertAgainstTrustedCAs(RegistrationResponseInterface $response): void
+ {
+ $pem = $response->getAttestationCertificate()->getPemFormatted();
+
+ $result = openssl_x509_checkpurpose(
+ $pem,
+ \X509_PURPOSE_ANY,
+ $this->trustedCAs
+ );
+ if ($result !== true) {
+ throw new SE(SE::NO_TRUSTED_CA);
+ }
}
}
diff --git a/src/SignRequest.php b/src/SignRequest.php
index 4dd3f3c..26509f7 100644
--- a/src/SignRequest.php
+++ b/src/SignRequest.php
@@ -4,14 +4,15 @@
use JsonSerializable;
-class SignRequest implements JsonSerializable, ChallengeProvider
+class SignRequest implements JsonSerializable, ChallengeProvider, KeyHandleInterface
{
use AppIdTrait;
use ChallengeTrait;
use KeyHandleTrait;
use VersionTrait;
- public function jsonSerialize() {
+ public function jsonSerialize()
+ {
return [
"version" => $this->version,
"challenge" => $this->getChallenge(),
@@ -19,5 +20,4 @@ public function jsonSerialize() {
"appId" => $this->getAppId(),
];
}
-
}
diff --git a/src/SignResponse.php b/src/SignResponse.php
index e28c14b..ecf2a04 100644
--- a/src/SignResponse.php
+++ b/src/SignResponse.php
@@ -5,22 +5,54 @@
use Firehed\U2F\InvalidDataException as IDE;
-class SignResponse
+class SignResponse implements LoginResponseInterface
{
use ResponseTrait;
// Decoded SignatureData
+ /** @var int */
private $counter = -1;
+
+ /** @var int */
private $user_presence = 0;
- public function getCounter(): int {
+ public function getCounter(): int
+ {
return $this->counter;
}
- public function getUserPresenceByte(): int {
+
+ public function getUserPresenceByte(): int
+ {
return $this->user_presence;
}
- protected function parseResponse(array $response): self {
+ public function getSignedData(): string
+ {
+ // U2F Spec:
+ // https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success
+ return sprintf(
+ '%s%s%s%s',
+ $this->clientData->getApplicationParameter(),
+ chr($this->getUserPresenceByte()),
+ pack('N', $this->getCounter()),
+ // Note: Spec says this should be from the request, but that's not
+ // actually available via the JS API. Because we assert the
+ // challenge *value* from the Client Data matches the trusted one
+ // from the SignRequest and that value is included in the Challenge
+ // Parameter, this is safe unless/until SHA-256 is broken.
+ $this->clientData->getChallengeParameter()
+ );
+ }
+
+ /**
+ * @param array{
+ * keyHandle: string,
+ * clientData: string,
+ * signatureData: string,
+ * } $response
+ */
+ protected function parseResponse(array $response): self
+ {
$this->validateKeyInArray('keyHandle', $response);
$this->setKeyHandle(fromBase64Web($response['keyHandle']));
@@ -34,10 +66,15 @@ protected function parseResponse(array $response): self {
throw new IDE(IDE::MALFORMED_DATA, 'signatureData');
}
$decoded = unpack('cpresence/Ncounter/a*signature', $sig_raw);
+ assert($decoded !== false);
$this->user_presence = $decoded['presence'];
$this->counter = $decoded['counter'];
$this->setSignature($decoded['signature']);
return $this;
}
+ public function getChallenge(): string
+ {
+ return $this->clientData->getChallenge();
+ }
}
diff --git a/src/VersionTrait.php b/src/VersionTrait.php
index a65f66a..6318686 100644
--- a/src/VersionTrait.php
+++ b/src/VersionTrait.php
@@ -5,9 +5,11 @@
trait VersionTrait
{
+ /** @var 'U2F_V2' */
private $version = 'U2F_V2';
- public function getVersion(): string {
+ public function getVersion(): string
+ {
return $this->version;
}
}
diff --git a/src/functions.php b/src/functions.php
index ddbbb3b..8fe26e7 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -4,54 +4,22 @@
/**
* Takes a web-safe base64 encoded string and returns the decoded version
- * @param the encoded string
- * @return the raw binary string
+ * @param string $base64 the encoded string
+ * @return string the raw binary string
*/
-function fromBase64Web(string $base64): string {
- return base64_decode(strtr($base64, '-_', '+/'));
+function fromBase64Web(string $base64): string
+{
+ $decoded = base64_decode(strtr($base64, '-_', '+/'));
+ assert($decoded !== false);
+ return $decoded;
}
/**
* Encodes any string to web-safe base64
- * @param the raw binary string
- * @return the encoded string
+ * @param string $binary the raw binary string
+ * @return string the encoded string
*/
-function toBase64Web(string $binary): string {
+function toBase64Web(string $binary): string
+{
return rtrim(strtr(base64_encode($binary), '+/', '-_'), '=');
}
-
-// Multibyte string wrappers: hijack calls to `strlen` and `substr` to force
-// 8-bit encoding in the event that `mbstring.func_overload` parameter is
-// non-zero and the mbstring default charset is not 8bit.
-
-/**
- * Identical to `\strlen` except when `mbstring.func_overload` is enabled and
- * set to a multi-byte character set, in which case it retains the
- * non-overloaded behavior.
- *
- * @param string The string being measured
- * @return int The length of the string, in bytes
- */
-function strlen(string $string): int {
- if (function_exists('mb_strlen')) {
- return \mb_strlen($string, '8bit');
- }
- return \strlen($string);
-}
-
-/**
- * Identical to `\substr` except when `mbstring.func_overload` is enabled and
- * set to a multi-byte character set, in which case it retains the
- * non-overloaded behavior.
- *
- * @param string The input string
- * @param int The starting point, in bytes
- * @param int The length, in bytes
- * @return string The extracted part of the string
- */
-function substr(string $string, int $start, int $length = null): string {
- if (function_exists('mb_substr')) {
- return \mb_substr($string, $start, $length, '8bit');
- }
- return \substr($string, $start, $length);
-}
diff --git a/tests/AppIdTraitTest.php b/tests/AppIdTraitTest.php
index 25ca4e4..8feaafa 100644
--- a/tests/AppIdTraitTest.php
+++ b/tests/AppIdTraitTest.php
@@ -8,35 +8,63 @@
* @covers ::
* @covers ::
*/
-class AppIdTraitTest extends \PHPUnit_Framework_TestCase
+class AppIdTraitTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::getAppId
* @covers ::setAppId
*/
- public function testAccessors() {
+ public function testAccessors(): void
+ {
$obj = new class {
use AppIdTrait;
};
$appId = 'https://u2f.example.com';
- $this->assertSame($obj, $obj->setAppId($appId),
- 'setAppId should return $this');
- $this->assertSame($appId, $obj->getAppId(),
- 'getAppId should return the set value');
+ $this->assertSame(
+ $obj,
+ $obj->setAppId($appId),
+ 'setAppId should return $this'
+ );
+ $this->assertSame(
+ $appId,
+ $obj->getAppId(),
+ 'getAppId should return the set value'
+ );
}
/**
* @covers ::getApplicationParameter
*/
- public function testGetApplicationParameter() {
- $obj = new class { use AppIdTrait; };
+ public function testGetApplicationParameter(): void
+ {
+ $obj = new class {
+ use AppIdTrait;
+ };
$appId = 'https://u2f.example.com';
$obj->setAppId($appId);
- $this->assertSame(hash('sha256', $appId, true),
+ $this->assertSame(
+ hash('sha256', $appId, true),
$obj->getApplicationParameter(),
- 'getApplicationParamter should return the raw SHA256 hash of the application id');
+ 'getApplicationParamter should return the raw SHA256 hash of the application id'
+ );
}
+ /**
+ * @covers ::getRpIdHash
+ */
+ public function testGetRpIdHash(): void
+ {
+ $obj = new class {
+ use AppIdTrait;
+ };
+ $appId = 'https://u2f.example.com';
+ $obj->setAppId($appId);
+ $this->assertSame(
+ hash('sha256', $appId, true),
+ $obj->getRpIdHash(),
+ 'getRpIdHash should return the raw SHA256 hash of the application id'
+ );
+ }
}
diff --git a/tests/AttestationCertificateTest.php b/tests/AttestationCertificateTest.php
new file mode 100644
index 0000000..7c7b235
--- /dev/null
+++ b/tests/AttestationCertificateTest.php
@@ -0,0 +1,67 @@
+
+ * @covers ::
+ */
+class AttestationCertificateTest extends \PHPUnit\Framework\TestCase
+{
+ /**
+ * @covers ::__construct
+ */
+ public function testConstruct(): void
+ {
+ // Note: a future, stricter implementation which actually parses and
+ // examines the ASN.1 format should fail on this. For now it's just
+ // a simple bag of bytes.
+ $raw = random_bytes(128);
+ $cert = new AttestationCertificate($raw);
+ $this->assertInstanceOf(AttestationCertificateInterface::class, $cert);
+ }
+
+ /**
+ * @covers ::getBinary
+ */
+ public function testGetBinary(): void
+ {
+ $raw = random_bytes(128);
+ $cert = new AttestationCertificate($raw);
+ $this->assertSame(
+ $raw,
+ $cert->getBinary(),
+ 'getBinary should return the untouched raw data'
+ );
+ }
+
+ /**
+ * @covers ::getPemFormatted
+ */
+ public function testGetPemFormatted(): void
+ {
+ $raw = random_bytes(128);
+ $cert = new AttestationCertificate($raw);
+ $expected = "-----BEGIN CERTIFICATE-----\r\n";
+ $expected .= chunk_split(base64_encode($raw), 64);
+ $expected .= "-----END CERTIFICATE-----";
+ $this->assertSame(
+ $expected,
+ $cert->getPemFormatted(),
+ 'PEM-formatted certificate was incorrect'
+ );
+ }
+
+ /**
+ * @covers ::__debugInfo
+ */
+ public function testDebugInfoEncodesBinary(): void
+ {
+ $cert = new AttestationCertificate(random_bytes(128));
+ $debugInfo = $cert->__debugInfo();
+ $this->assertArrayHasKey('binary', $debugInfo);
+ $this->assertRegExp('/^0x[0-9a-f]{256}$/', $debugInfo['binary']);
+ }
+}
diff --git a/tests/AttestationCertificateTraitTest.php b/tests/AttestationCertificateTraitTest.php
deleted file mode 100644
index bd5466a..0000000
--- a/tests/AttestationCertificateTraitTest.php
+++ /dev/null
@@ -1,92 +0,0 @@
-
- * @covers ::
- */
-class AttestationCertificateTraitTest extends \PHPUnit_Framework_TestCase
-{
- /**
- * @covers ::getAttestationCertificateBinary
- * @covers ::setAttestationCertificate
- */
- public function testAccessors() {
- $obj = new class {
- use AttestationCertificateTrait;
- };
- $cert = bin2hex(random_bytes(35));
- $this->assertSame($obj, $obj->setAttestationCertificate($cert),
- 'setAttestationCertificate should return $this');
- $this->assertSame($cert, $obj->getAttestationCertificateBinary(),
- 'getAttestationCertificate should return the challenge that was set');
- }
-
- /**
- * @covers ::getAttestationCertificatePem
- */
- public function testGetAttestationCertificatePem() {
- $obj = new class {
- use AttestationCertificateTrait;
- };
- $raw = random_bytes(128);
- $expected = "-----BEGIN CERTIFICATE-----\r\n";
- $expected .= chunk_split(base64_encode($raw), 64);
- $expected .= "-----END CERTIFICATE-----";
- $obj->setAttestationCertificate($raw);
- $this->assertSame($expected, $obj->getAttestationCertificatePem(),
- 'PEM-formatted certificate was incorrect');
- }
-
- /**
- * @covers ::verifyIssuerAgainstTrustedCAs
- */
- public function testSuccessfulCAVerification() {
- $class = $this->getObjectWithYubicoCert();
- $certs = [dirname(__DIR__).'/CAcerts/yubico.pem'];
- $this->assertTrue($class->verifyIssuerAgainstTrustedCAs($certs));
- }
-
- /**
- * @covers ::verifyIssuerAgainstTrustedCAs
- */
- public function testFailedCAVerification() {
- $class = $this->getObjectWithYubicoCert();
- $certs = [__DIR__.'/verisign_only_for_unit_tests.pem'];
- $this->expectException(SecurityException::class);
- $this->expectExceptionCode(SecurityException::NO_TRUSTED_CA);
- $class->verifyIssuerAgainstTrustedCAs($certs);
- }
-
- /**
- * @covers ::verifyIssuerAgainstTrustedCAs
- */
- public function testFailedCAVerificationFromNoCAs() {
- $class = $this->getObjectWithYubicoCert();
- $certs = [];
- $this->expectException(SecurityException::class);
- $this->expectExceptionCode(SecurityException::NO_TRUSTED_CA);
- $class->verifyIssuerAgainstTrustedCAs($certs);
- }
-
- // -(Helper methods)-------------------------------------------------------
-
- /**
- * Returns some concrete implementation of a class using
- * AttestationCertificateTrait
- *
- * @return mixed Some class using AttestationCertificateTrait
- */
- private function getObjectWithYubicoCert() {
- $response = RegisterResponse::fromJson(
- file_get_contents(__DIR__.'/register_response.json'));
- // Sanity check that the response actually imlements this trait, rather
- // than doing all sorts of magic
- $check = AttestationCertificateTrait::class;
- $this->assertContains($check, class_uses($response));
- return $response;
- }
-}
diff --git a/tests/ChallengeTraitTest.php b/tests/ChallengeTraitTest.php
index f026e84..a2a8a37 100644
--- a/tests/ChallengeTraitTest.php
+++ b/tests/ChallengeTraitTest.php
@@ -8,21 +8,28 @@
* @covers ::
* @covers ::
*/
-class ChallengeTraitTest extends \PHPUnit_Framework_TestCase
+class ChallengeTraitTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::getChallenge
* @covers ::setChallenge
*/
- public function testAccessors() {
+ public function testAccessors(): void
+ {
$obj = new class {
use ChallengeTrait;
};
$challenge = bin2hex(random_bytes(15));
- $this->assertSame($obj, $obj->setChallenge($challenge),
- 'setChallenge should return $this');
- $this->assertSame($challenge, $obj->getChallenge(),
- 'getChallenge should return the challenge that was set');
+ $this->assertSame(
+ $obj,
+ $obj->setChallenge($challenge),
+ 'setChallenge should return $this'
+ );
+ $this->assertSame(
+ $challenge,
+ $obj->getChallenge(),
+ 'getChallenge should return the challenge that was set'
+ );
}
}
diff --git a/tests/ClientDataTest.php b/tests/ClientDataTest.php
index b9c5f1a..2ffc3b2 100644
--- a/tests/ClientDataTest.php
+++ b/tests/ClientDataTest.php
@@ -8,39 +8,76 @@
* @covers ::
* @covers ::
*/
-class ClientDataTest extends \PHPUnit_Framework_TestCase
+class ClientDataTest extends \PHPUnit\Framework\TestCase
{
-
/**
* @covers ::fromJson
*/
- public function testFromValidJson() {
- $goodJson = '{"typ":"navigator.id.finishEnrollment","challenge":"PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo","origin":"https://u2f.ericstern.com","cid_pubkey":""}';
+ public function testFromValidJson(): void
+ {
+ $goodData = [
+ 'typ' => 'navigator.id.finishEnrollment',
+ 'challenge' => 'PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo',
+ 'origin' => 'https://u2f.ericstern.com',
+ 'cid_pubkey' => '',
+ ];
+ $goodJson = json_encode($goodData);
+ assert($goodJson !== false);
$clientData = ClientData::fromJson($goodJson);
$this->assertInstanceOf(ClientData::class, $clientData);
}
/**
* @covers ::getChallengeParameter
- * @covers ::jsonSerialize
*/
- public function testGetChallengeParameter() {
+ public function testGetChallengeParameter(): void
+ {
$expected_param = base64_decode('exDPjyyKbizXMAAUNLpv0QYJNyXClbUqewUWojPtp0g=');
+ assert($expected_param !== false);
// Sanity check
- $this->assertSame(32,
+ $this->assertSame(
+ 32,
strlen($expected_param),
- 'Test vector should have been 32 bytes');
+ 'Test vector should have been 32 bytes'
+ );
+
+ $goodJson = '{"typ":"navigator.id.finishEnrollment","challenge":"PfsWR'.
+ '1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo","origin":"https://u2f.eri'.
+ 'cstern.com","cid_pubkey":""}';
- $goodJson = '{"typ":"navigator.id.finishEnrollment","challenge":"PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo","origin":"https://u2f.ericstern.com","cid_pubkey":""}';
+ assert($goodJson !== false);
$clientData = ClientData::fromJson($goodJson);
- $this->assertTrue(hash_equals($expected_param, $clientData->getChallengeParameter()),
- 'Challenge parameter did not match expected value');
+ $this->assertTrue(
+ hash_equals($expected_param, $clientData->getChallengeParameter()),
+ 'Challenge parameter did not match expected value'
+ );
+ }
+
+ /**
+ * @covers ::getApplicationParameter
+ */
+ public function testGetApplicationParameter(): void
+ {
+ $goodData = [
+ 'typ' => 'navigator.id.finishEnrollment',
+ 'challenge' => 'PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo',
+ 'origin' => 'https://u2f.ericstern.com',
+ 'cid_pubkey' => '',
+ ];
+ $goodJson = json_encode($goodData);
+ assert($goodJson !== false);
+ $clientData = ClientData::fromJson($goodJson);
+ $this->assertSame(
+ hash('sha256', 'https://u2f.ericstern.com', true),
+ $clientData->getApplicationParameter()
+ );
}
/**
* @covers ::fromJson
*/
- public function testBadJson() {
+ public function testBadJson(): void
+ {
$json = 'this is not json';
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
@@ -51,7 +88,8 @@ public function testBadJson() {
* @covers ::fromJson
* @dataProvider missingData
*/
- public function testDataValidation($json) {
+ public function testDataValidation(string $json): void
+ {
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MISSING_KEY);
ClientData::fromJson($json);
@@ -60,7 +98,8 @@ public function testDataValidation($json) {
/**
* @dataProvider types
*/
- public function testTypes(string $type, bool $allowed) {
+ public function testTypes(string $type, bool $allowed): void
+ {
$all = [
'typ' => $type,
'challenge' => 'SOMECHALLENGE',
@@ -68,6 +107,7 @@ public function testTypes(string $type, bool $allowed) {
'cid_pubkey' => '',
];
$json = json_encode($all);
+ assert($json !== false);
if (!$allowed) {
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
@@ -79,26 +119,33 @@ public function testTypes(string $type, bool $allowed) {
// -( DataProviders )------------------------------------------------------
- public function missingData(): array {
+ /**
+ * @return array{string}[]
+ */
+ public function missingData(): array
+ {
$all = [
'typ' => 'navigator.id.finishEnrollment',
'challenge' => 'SOMECHALLENGE',
'origin' => 'https://u2f.example.com',
'cid_pubkey' => '',
];
- $without = function(string $i) use ($all): array {
+ $without = function (string $i) use ($all): array {
unset($all[$i]);
- return [json_encode($all)];
+ return [json_encode($all, JSON_THROW_ON_ERROR)];
};
return [
$without('typ'),
$without('challenge'),
$without('origin'),
- $without('cid_pubkey'),
];
}
- public function types(): array {
+ /**
+ * @return array{string, bool}[]
+ */
+ public function types(): array
+ {
return [
['navigator.id.getAssertion', true],
['navigator.id.finishEnrollment', true],
diff --git a/tests/ClientErrorExceptionTest.php b/tests/ClientErrorExceptionTest.php
index eecbe1f..a32ee94 100644
--- a/tests/ClientErrorExceptionTest.php
+++ b/tests/ClientErrorExceptionTest.php
@@ -8,23 +8,30 @@
* @covers ::
* @covers ::
*/
-class ClientErrorExceptionTest extends \PHPUnit_Framework_TestCase
+class ClientErrorExceptionTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::__construct
* @dataProvider clientErrors
*/
- public function testClientError(int $code) {
+ public function testClientError(int $code): void
+ {
$ex = new ClientErrorException($code);
- $this->assertInstanceOf(ClientErrorException::class,
+ $this->assertInstanceOf(
+ ClientErrorException::class,
$ex,
- '__construct failed');
- $this->assertNotEmpty($ex->getMessage(),
- 'A predefined message should have been used');
+ '__construct failed'
+ );
+ $this->assertNotEmpty(
+ $ex->getMessage(),
+ 'A predefined message should have been used'
+ );
}
// -( DataProviders )------------------------------------------------------
- public function clientErrors() {
+ /** @return array{int}[] */
+ public function clientErrors()
+ {
return [
[ClientError::OTHER_ERROR],
[ClientError::BAD_REQUEST],
@@ -33,6 +40,4 @@ public function clientErrors() {
[ClientError::TIMEOUT],
];
}
-
-
}
diff --git a/tests/ECPublicKeyTest.php b/tests/ECPublicKeyTest.php
new file mode 100644
index 0000000..99b569d
--- /dev/null
+++ b/tests/ECPublicKeyTest.php
@@ -0,0 +1,104 @@
+
+ * @covers ::
+ */
+class ECPublicKeyTest extends \PHPUnit\Framework\TestCase
+{
+ /**
+ * @covers ::__construct
+ */
+ public function testConstruct(): void
+ {
+ $key = "\x04".\random_bytes(64);
+ $obj = new ECPublicKey($key);
+ $this->assertInstanceOf(PublicKeyInterface::class, $obj);
+ }
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstructThrowsWithBadFirstByte(): void
+ {
+ $key = "\x01".\random_bytes(64);
+ $this->expectException(InvalidDataException::class);
+ $this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
+ new ECPublicKey($key);
+ }
+
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstructThrowsWhenTooShort(): void
+ {
+ $key = "\x04".random_bytes(63);
+ $this->expectException(InvalidDataException::class);
+ $this->expectExceptionCode(InvalidDataException::PUBLIC_KEY_LENGTH);
+ new ECPublicKey($key);
+ }
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstructThrowsWhenTooLong(): void
+ {
+ $key = "\x04".random_bytes(65);
+ $this->expectException(InvalidDataException::class);
+ $this->expectExceptionCode(InvalidDataException::PUBLIC_KEY_LENGTH);
+ new ECPublicKey($key);
+ }
+
+ /**
+ * @covers ::getBinary
+ */
+ public function testGetBinary(): void
+ {
+ $key = "\x04".\random_bytes(64);
+ $obj = new ECPublicKey($key);
+ $this->assertSame(
+ $key,
+ $obj->getBinary(),
+ 'getBinary should return the set value'
+ );
+ }
+
+ /**
+ * @covers ::getPemFormatted
+ */
+ public function testGetPublicKeyPem(): void
+ {
+ $key = hex2bin(
+ '04b4960ae0fa301033fbedc85c33ac30408dffd6098bc8580d8b66159959d89b9'.
+ '31daf1d43a1949b07b7d47eea25efcac478bb5cd6ead0a3c3f7b7cb2a7bc1e3be'
+ );
+ assert($key !== false);
+ $obj = new ECPublicKey($key);
+ $pem =
+ "-----BEGIN PUBLIC KEY-----\r\n".
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtJYK4PowEDP77chcM6wwQI3/1gmL\r\n".
+ "yFgNi2YVmVnYm5Mdrx1DoZSbB7fUfuol78rEeLtc1urQo8P3t8sqe8Hjvg==\r\n".
+ "-----END PUBLIC KEY-----";
+
+ $this->assertSame($pem, $obj->getPemFormatted());
+ }
+
+ /**
+ * @covers ::__debugInfo
+ */
+ public function testDebugInfoEncodesBinary(): void
+ {
+ $x = random_bytes(32);
+ $y = random_bytes(32);
+ $pk = new ECPublicKey("\x04$x$y");
+ $debugInfo = $pk->__debugInfo();
+ $this->assertArrayHasKey('x', $debugInfo);
+ $this->assertSame('0x'.bin2hex($x), $debugInfo['x'], 'x-coordinate wrong');
+ $this->assertSame('0x'.bin2hex($y), $debugInfo['y'], 'y-coordinate wrong');
+ }
+}
diff --git a/tests/ECPublicKeyTraitTest.php b/tests/ECPublicKeyTraitTest.php
deleted file mode 100644
index 9daacc0..0000000
--- a/tests/ECPublicKeyTraitTest.php
+++ /dev/null
@@ -1,93 +0,0 @@
-
- * @covers ::
- */
-class ECPublicKeyTraitTest extends \PHPUnit_Framework_TestCase
-{
-
- /**
- * @covers ::setPublicKey
- * @covers ::getPublicKey
- */
- public function testAccessors() {
- $obj = new class {
- use ECPublicKeyTrait;
- };
- $key = "\x04".\random_bytes(64);
- $this->assertSame($obj, $obj->setPublicKey($key),
- 'setPublicKey should return $this');
- $this->assertSame($key, $obj->getPublicKey(),
- 'getPublicKey should return the set value');
- }
-
- /**
- * @covers ::getPublicKeyPem
- */
- public function testGetPublicKeyPem() {
- $obj = new class {
- use ECPublicKeyTrait;
- };
-
- $key = hex2bin(
- '04b4960ae0fa301033fbedc85c33ac30408dffd6098bc8580d8b66159959d89b9'.
- '31daf1d43a1949b07b7d47eea25efcac478bb5cd6ead0a3c3f7b7cb2a7bc1e3be'
- );
- $pem =
- "-----BEGIN PUBLIC KEY-----\r\n".
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtJYK4PowEDP77chcM6wwQI3/1gmL\r\n".
- "yFgNi2YVmVnYm5Mdrx1DoZSbB7fUfuol78rEeLtc1urQo8P3t8sqe8Hjvg==\r\n".
- "-----END PUBLIC KEY-----";
-
- $obj->setPublicKey($key);
- $this->assertSame($pem, $obj->getPublicKeyPem());
- }
-
- /**
- * @covers ::setPublicKey
- */
- public function testSetPublicKeyThrowsWithBadFirstByte() {
- $obj = new class {
- use ECPublicKeyTrait;
- };
- $key = "\x01".\random_bytes(64);
- $this->expectException(InvalidDataException::class);
- $this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
- $obj->setPublicKey($key);
- }
-
-
- /**
- * @covers ::setPublicKey
- */
- public function testSetPublicKeyThrowsWhenTooShort() {
- $obj = new class {
- use ECPublicKeyTrait;
- };
- $key = "\x04".random_bytes(63);
- $this->expectException(InvalidDataException::class);
- $this->expectExceptionCode(InvalidDataException::PUBLIC_KEY_LENGTH);
- $obj->setPublicKey($key);
- }
-
- /**
- * @covers ::setPublicKey
- */
- public function testSetPublicKeyThrowsWhenTooLong() {
- $obj = new class {
- use ECPublicKeyTrait;
- };
- $key = "\x04".random_bytes(65);
- $this->expectException(InvalidDataException::class);
- $this->expectExceptionCode(InvalidDataException::PUBLIC_KEY_LENGTH);
- $obj->setPublicKey($key);
- }
-
-
-
-}
diff --git a/tests/functionsTest.php b/tests/FunctionsTest.php
similarity index 58%
rename from tests/functionsTest.php
rename to tests/FunctionsTest.php
index 85b9e2a..da61992 100644
--- a/tests/functionsTest.php
+++ b/tests/FunctionsTest.php
@@ -3,29 +3,37 @@
namespace Firehed\U2F;
-class functionsTest extends \PHPUnit_Framework_TestCase
+class FunctionsTest extends \PHPUnit\Framework\TestCase
{
-
-
/**
* @covers Firehed\U2F\fromBase64Web
* @dataProvider vectors
*/
- public function testFromBase64Web($plain, $encoded) {
- $this->assertSame($plain, fromBase64Web($encoded),
- 'Decoded vector did not match known plaintext version');
+ public function testFromBase64Web(string $plain, string $encoded): void
+ {
+ $this->assertSame(
+ $plain,
+ fromBase64Web($encoded),
+ 'Decoded vector did not match known plaintext version'
+ );
}
/**
* @covers Firehed\U2F\toBase64Web
* @dataProvider vectors
*/
- public function testToBase64Web($plain, $encoded) {
- $this->assertSame($encoded, toBase64Web($plain),
- 'Encoded vector did not match known version');
+ public function testToBase64Web(string $plain, string $encoded): void
+ {
+ $this->assertSame(
+ $encoded,
+ toBase64Web($plain),
+ 'Encoded vector did not match known version'
+ );
}
- public function vectors(): array {
+ /** @return array{string, string}[] */
+ public function vectors(): array
+ {
return [
// Plain Encoded
// Adapted test vctors from RFC4648 Sec. 5 (padding trimmed)
diff --git a/tests/InvalidDataExceptionTest.php b/tests/InvalidDataExceptionTest.php
index 933b9d9..012d8fb 100644
--- a/tests/InvalidDataExceptionTest.php
+++ b/tests/InvalidDataExceptionTest.php
@@ -8,30 +8,39 @@
* @covers ::
* @covers ::
*/
-class InvalidDataExceptionTest extends \PHPUnit_Framework_TestCase
+class InvalidDataExceptionTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::__construct
- * @dataProvider invalidDataExceptionCodes
+ * @dataProvider invalidDataExceptionCodes
*/
- public function testInvalidDataException(int $code) {
+ public function testInvalidDataException(int $code): void
+ {
$ex = new InvalidDataException($code, 'Prefill');
- $this->assertInstanceOf(InvalidDataException::class,
+ $this->assertInstanceOf(
+ InvalidDataException::class,
$ex,
- '__construct failed');
- $this->assertNotEmpty($ex->getMessage(),
- 'A predefined message should have been used');
- $this->assertRegExp('/Prefill/', $ex->getMessage(),
- 'The sprintf args were not added to the exception message');
+ '__construct failed'
+ );
+ $this->assertNotEmpty(
+ $ex->getMessage(),
+ 'A predefined message should have been used'
+ );
+ $this->assertRegExp(
+ '/Prefill/',
+ $ex->getMessage(),
+ 'The sprintf args were not added to the exception message'
+ );
}
// -( DataProviders )------------------------------------------------------
- public function invalidDataExceptionCodes() {
+ /** @return array{int}[] */
+ public function invalidDataExceptionCodes()
+ {
return [
[InvalidDataException::MISSING_KEY],
[InvalidDataException::MALFORMED_DATA],
[InvalidDataException::PUBLIC_KEY_LENGTH],
];
}
-
}
diff --git a/tests/KeyHandleTraitTest.php b/tests/KeyHandleTraitTest.php
index 6723e44..26c1aa9 100644
--- a/tests/KeyHandleTraitTest.php
+++ b/tests/KeyHandleTraitTest.php
@@ -8,35 +8,45 @@
* @covers ::
* @covers ::
*/
-class KeyHandleTraitTest extends \PHPUnit_Framework_TestCase
+class KeyHandleTraitTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::getKeyHandleBinary
* @covers ::setKeyHandle
*/
- public function testAccessors() {
+ public function testAccessors(): void
+ {
$obj = new class {
use KeyHandleTrait;
};
$kh = random_bytes(14)."\x00 ";
- $this->assertSame($obj, $obj->setKeyHandle($kh),
- 'setKeyHandle should return $this');
- $this->assertSame($kh, $obj->getKeyHandleBinary(),
- 'getKeyHandleBinary should return the set value');
+ $this->assertSame(
+ $obj,
+ $obj->setKeyHandle($kh),
+ 'setKeyHandle should return $this'
+ );
+ $this->assertSame(
+ $kh,
+ $obj->getKeyHandleBinary(),
+ 'getKeyHandleBinary should return the set value'
+ );
}
/**
* @covers ::getKeyHandleWeb
*/
- public function testGetKeyHandleWeb() {
+ public function testGetKeyHandleWeb(): void
+ {
$obj = new class {
use KeyHandleTrait;
};
$kh = random_bytes(14)."\x00 ";
$webKh = toBase64Web($kh);
$obj->setKeyHandle($kh);
- $this->assertSame($webKh, $obj->getKeyHandleWeb(),
- 'getKeyHandleWeb was encoded wrong');
+ $this->assertSame(
+ $webKh,
+ $obj->getKeyHandleWeb(),
+ 'getKeyHandleWeb was encoded wrong'
+ );
}
-
}
diff --git a/tests/RegisterRequestTest.php b/tests/RegisterRequestTest.php
index 9eb3a13..54eb2d8 100644
--- a/tests/RegisterRequestTest.php
+++ b/tests/RegisterRequestTest.php
@@ -8,13 +8,14 @@
* @covers ::
* @covers ::
*/
-class RegisterRequestTest extends \PHPUnit_Framework_TestCase
+class RegisterRequestTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::jsonSerialize
*/
- public function testJsonSerialize() {
+ public function testJsonSerialize(): void
+ {
$appId = 'https://u2f.example.com';
$challenge = 'some-random-string';
@@ -23,21 +24,37 @@ public function testJsonSerialize() {
->setAppId($appId)
->setChallenge($challenge);
$json = json_encode($request);
+ assert($json !== false);
$decoded = json_decode($json, true);
- $this->assertSame($appId, $request->getAppId(),
- 'getAppId returned the wrong value');
- $this->assertSame($appId, $decoded['appId'],
- 'json appId property did not match');
- $this->assertSame($challenge, $request->getChallenge(),
- 'getChallenge returned the wrong value');
- $this->assertSame($challenge, $decoded['challenge'],
- 'json challenge property did not match');
- $this->assertSame('U2F_V2', $request->getVersion(),
- 'getVersion returned the wrong value');
- $this->assertSame('U2F_V2', $decoded['version'],
- 'json version was incorrect');
-
-
+ $this->assertSame(
+ $appId,
+ $request->getAppId(),
+ 'getAppId returned the wrong value'
+ );
+ $this->assertSame(
+ $appId,
+ $decoded['appId'],
+ 'json appId property did not match'
+ );
+ $this->assertSame(
+ $challenge,
+ $request->getChallenge(),
+ 'getChallenge returned the wrong value'
+ );
+ $this->assertSame(
+ $challenge,
+ $decoded['challenge'],
+ 'json challenge property did not match'
+ );
+ $this->assertSame(
+ 'U2F_V2',
+ $request->getVersion(),
+ 'getVersion returned the wrong value'
+ );
+ $this->assertSame(
+ 'U2F_V2',
+ $decoded['version'],
+ 'json version was incorrect'
+ );
}
-
}
diff --git a/tests/RegisterResponseTest.php b/tests/RegisterResponseTest.php
index 4e09a50..c8d6feb 100644
--- a/tests/RegisterResponseTest.php
+++ b/tests/RegisterResponseTest.php
@@ -8,17 +8,46 @@
* @covers ::
* @covers ::
*/
-class RegisterResponseTest extends \PHPUnit_Framework_TestCase
+class RegisterResponseTest extends \PHPUnit\Framework\TestCase
{
- private $validClientData = 'eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6IkJyQWN4dGIxOWFYNTRoN0Y2T0NKWVptQ3prZHlHV0Nib3NEcHpNMUh2MkUiLCJvcmlnaW4iOiJodHRwczovL3UyZi5lcmljc3Rlcm4uY29tIiwiY2lkX3B1YmtleSI6IiJ9';
- private $validRegistrationData = "BQS55FfGvxbgmcNO1cpNhdr4r-CMSbMtuhiMMJbXqd_3FD8Aah2X_n4ZiyBlgBqbbe4RdyksR7ZXoqPYT47-tmeWQJhf7xs1T8ObBRpkFi_VWG5oFJe499mQYxcj9BR0G8B5fjkYbUuPCwNRiscOP8P18ep6V1OOulT3tq6kBC-94xQwggItMIIBF6ADAgECAgQFtgV5MAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKDEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgOTU4MTUwMzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT9uN6zoe1w62NsBm62AGmWpflw_LXbiPw7MF1B5ZZvDBtUuFL-8KCQftF_O__CnU0yG5z4qEos6qA4yr011ZjeoyYwJDAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTALBgkqhkiG9w0BAQsDggEBAH7T-2zMJSAT-C8hjCo32mAx0g5_MIHa_K6xKPx_myM5FL-2TWE18XziIfp2T0U-8Sc6jOlllWRCuy8eR0g_c33LyYtYU3f-9QsnDgKJ-IQ28a3PSbJiHuXjAt9VW5q3QnLgafkYFJs97E8SIosQwPiN42r1inS7RCuFrgBTZL2mcCBY_B8th5tTARHqYOhsY_F_pZRMyD8KommEiz7jiKbAnmsFlT_LuPR-g6J-AHKmPDKtZIZOkm1xEvoZl_eDllb7syvo94idDwFFUZonr92ORrBMpCkNhUC2NLiGFh51iMhimdzdZDXRZ4o6bwp0gpxN0_cMNSTR3fFteK3SG2QwRAIgFTLJPY9_a0ZPujRfLufS-9ANCWemIWPHqs3icavMJIgCIFH5MSGDFkuY_NWhKa4mbLdbP6r7wMwspwHPG5_Xf48V";
+ /** @var string */
+ private $validClientData =
+ 'eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZmluaXNoRW5yb2xsbWVudCIsImNoYWxsZW5nZSI6I'.
+ 'kJyQWN4dGIxOWFYNTRoN0Y2T0NKWVptQ3prZHlHV0Nib3NEcHpNMUh2MkUiLCJvcmlnaW'.
+ '4iOiJodHRwczovL3UyZi5lcmljc3Rlcm4uY29tIiwiY2lkX3B1YmtleSI6IiJ9';
+
+ /** @var string */
+ private $validRegistrationData =
+ 'BQS55FfGvxbgmcNO1cpNhdr4r-CMSbMtuhiMMJbXqd_3FD8Aah2X_n4ZiyBlgBqbbe4Rd'.
+ 'yksR7ZXoqPYT47-tmeWQJhf7xs1T8ObBRpkFi_VWG5oFJe499mQYxcj9BR0G8B5fjkYbU'.
+ 'uPCwNRiscOP8P18ep6V1OOulT3tq6kBC-94xQwggItMIIBF6ADAgECAgQFtgV5MAsGCSq'.
+ 'GSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIw'.
+ 'MDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKDEmMCQGA1UEAwwdW'.
+ 'XViaWNvIFUyRiBFRSBTZXJpYWwgOTU4MTUwMzMwWTATBgcqhkjOPQIBBggqhkjOPQMBBw'.
+ 'NCAAT9uN6zoe1w62NsBm62AGmWpflw_LXbiPw7MF1B5ZZvDBtUuFL-8KCQftF_O__CnU0'.
+ 'yG5z4qEos6qA4yr011ZjeoyYwJDAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgy'.
+ 'LjEuMTALBgkqhkiG9w0BAQsDggEBAH7T-2zMJSAT-C8hjCo32mAx0g5_MIHa_K6xKPx_m'.
+ 'yM5FL-2TWE18XziIfp2T0U-8Sc6jOlllWRCuy8eR0g_c33LyYtYU3f-9QsnDgKJ-IQ28a'.
+ '3PSbJiHuXjAt9VW5q3QnLgafkYFJs97E8SIosQwPiN42r1inS7RCuFrgBTZL2mcCBY_B8'.
+ 'th5tTARHqYOhsY_F_pZRMyD8KommEiz7jiKbAnmsFlT_LuPR-g6J-AHKmPDKtZIZOkm1x'.
+ 'EvoZl_eDllb7syvo94idDwFFUZonr92ORrBMpCkNhUC2NLiGFh51iMhimdzdZDXRZ4o6b'.
+ 'wp0gpxN0_cMNSTR3fFteK3SG2QwRAIgFTLJPY9_a0ZPujRfLufS-9ANCWemIWPHqs3ica'.
+ 'vMJIgCIFH5MSGDFkuY_NWhKa4mbLdbP6r7wMwspwHPG5_Xf48V';
/**
* @covers ::fromJson
*/
- public function testFromJson() {
- $json = sprintf('{"registrationData":"%s","version":"U2F_V2","challenge":"BrAcxtb19aX54h7F6OCJYZmCzkdyGWCbosDpzM1Hv2E","appId":"https://u2f.ericstern.com","clientData":"%s"}', $this->validRegistrationData, $this->validClientData);;
+ public function testFromJson(): void
+ {
+ $json = json_encode([
+ 'registrationData' => $this->validRegistrationData,
+ 'version' => 'U2F_V2',
+ 'challenge' => 'BrAcxtb19aX54h7F6OCJYZmCzkdyGWCbosDpzM1Hv2E',
+ 'appId' => 'https://u2f.ericstern.com',
+ 'clientData' => $this->validClientData,
+ ]);
+ assert($json !== false);
$response = RegisterResponse::fromJson($json);
$this->assertInstanceOf(RegisterResponse::class, $response);
}
@@ -26,21 +55,24 @@ public function testFromJson() {
/**
* @dataProvider clientErrors
*/
- public function testErrorResponse(int $code) {
+ public function testErrorResponse(int $code): void
+ {
$json = sprintf('{"errorCode":%d}', $code);
$this->expectException(ClientErrorException::class);
$this->expectExceptionCode($code);
RegisterResponse::fromJson($json);
}
- public function testFromJsonBadJson() {
+ public function testFromJsonBadJson(): void
+ {
$json = 'this is not json';
$this->expectException(InvalidDataException::class);
// FIXME: code
RegisterResponse::fromJson($json);
}
- public function testFromJsonMissingClientData() {
+ public function testFromJsonMissingClientData(): void
+ {
$json = sprintf('{"registrationData":"%s"}', $this->validRegistrationData);
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MISSING_KEY);
@@ -48,7 +80,8 @@ public function testFromJsonMissingClientData() {
RegisterResponse::fromJson($json);
}
- public function testFromJsonMissingRegistrationData() {
+ public function testFromJsonMissingRegistrationData(): void
+ {
$json = sprintf('{"clientData":"%s"}', $this->validClientData);
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MISSING_KEY);
@@ -59,7 +92,8 @@ public function testFromJsonMissingRegistrationData() {
/**
* @dataProvider invalidRegistrationData
*/
- public function testBadRegistrationData(string $registrationData) {
+ public function testBadRegistrationData(string $registrationData): void
+ {
$json = $this->buildJson($this->validClientData, $registrationData);
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
@@ -67,12 +101,13 @@ public function testBadRegistrationData(string $registrationData) {
}
/**
- * @covers ::getAttestationCertificateBinary
+ * @covers ::getAttestationCertificate
* @covers ::getKeyHandleBinary
* @covers ::getPublicKey
* @covers ::getSignature
*/
- public function testDataAccuracyAfterSuccessfulParsing() {
+ public function testDataAccuracyAfterSuccessfulParsing(): void
+ {
$pubkey = "\x04".random_bytes(64);
$handle = random_bytes(32);
$st = "\x05".$pubkey."\x20".$handle;
@@ -83,19 +118,96 @@ public function testDataAccuracyAfterSuccessfulParsing() {
$json = $this->buildJson($this->validClientData, $reg);
$response = RegisterResponse::fromJson($json);
- $this->assertSame($pubkey, $response->getPublicKey(),
- 'Public key was not parsed correctly');
- $this->assertSame($handle, $response->getKeyHandleBinary(),
- 'Key Handle was not parsed correctly');
- $this->assertSame($cert, $response->getAttestationCertificateBinary(),
- 'Cert was not parsed correctly');
- $this->assertSame($sig, $response->getSignature(),
- 'Signature was not parsed correctly');
+ $this->assertSame(
+ $pubkey,
+ $response->getPublicKey()->getBinary(),
+ 'Public key was not parsed correctly'
+ );
+ $this->assertSame(
+ $handle,
+ $response->getKeyHandleBinary(),
+ 'Key Handle was not parsed correctly'
+ );
+ $this->assertSame(
+ $cert,
+ $response->getAttestationCertificate()->getBinary(),
+ 'Cert was not parsed correctly'
+ );
+ $this->assertSame(
+ $sig,
+ $response->getSignature(),
+ 'Signature was not parsed correctly'
+ );
+ }
+
+ /**
+ * @covers ::getSignedData
+ */
+ public function testGetSignedData(): void
+ {
+ $json = file_get_contents(__DIR__ . '/register_response.json');
+ assert($json !== false);
+ $response = RegisterResponse::fromJson($json);
+
+ $expectedSignedData = sprintf(
+ '%s%s%s%s%s',
+ "\x00",
+ hash('sha256', 'https://u2f.ericstern.com', true),
+ hash(
+ 'sha256',
+ '{'.
+ '"typ":"navigator.id.finishEnrollment",'.
+ '"challenge":"PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo",'.
+ '"origin":"https://u2f.ericstern.com",'.
+ '"cid_pubkey":""'.
+ '}',
+ true
+ ),
+ $response->getKeyHandleBinary(),
+ $response->getPublicKey()->getBinary()
+ );
+
+ $this->assertSame(
+ $expectedSignedData,
+ $response->getSignedData(),
+ 'Wrong signed data'
+ );
+ }
+
+ /**
+ * @covers ::getChallenge
+ */
+ public function testGetChallenge(): void
+ {
+ $json = file_get_contents(__DIR__ . '/register_response.json');
+ assert($json !== false);
+ $response = RegisterResponse::fromJson($json);
+
+ $this->assertSame(
+ 'PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo',
+ $response->getChallenge()
+ );
+ }
+ /**
+ * @covers ::getRpIdHash
+ */
+ public function testGetRpIdHash(): void
+ {
+ $json = file_get_contents(__DIR__ . '/register_response.json');
+ assert($json !== false);
+ $response = RegisterResponse::fromJson($json);
+
+ $this->assertSame(
+ hash('sha256', 'https://u2f.ericstern.com', true),
+ $response->getRpIdHash()
+ );
}
// -( DataProviders )------------------------------------------------------
- public function clientErrors() {
+ /** @return array{int}[] */
+ public function clientErrors()
+ {
return [
[ClientError::OTHER_ERROR],
[ClientError::BAD_REQUEST],
@@ -105,9 +217,11 @@ public function clientErrors() {
];
}
- public function invalidRegistrationData(): array {
- $bad_reserved_byte = "\x01".str_repeat('a',200);
- $bad_pubkey_start = "\x05\x99".str_repeat('a',200);
+ /** @return array{string}[] */
+ public function invalidRegistrationData(): array
+ {
+ $bad_reserved_byte = "\x01".str_repeat('a', 200);
+ $bad_pubkey_start = "\x05\x99".str_repeat('a', 200);
$pubkey_too_short = "\x05\x04".random_bytes(5);
$handle_too_short = "\x05\x04".random_bytes(64)."\x20".random_bytes(16);
@@ -116,8 +230,8 @@ public function invalidRegistrationData(): array {
$valid_start = "\x05\x04".random_bytes(64)."\x20".random_bytes(32);
$bad_cert_start = "\x40".str_repeat('a', 100); // Must start with bxxx10000
$crazy_long_cert = "\x30\x85".str_repeat('a', 100);
- $too_short_cert = "\x30\x82\x01\x00".str_repeat('a',50); // x0100 bytes long
- return array_map(function(string $s) {
+ $too_short_cert = "\x30\x82\x01\x00".str_repeat('a', 50); // x0100 bytes long
+ return array_map(function (string $s) {
return [toBase64Web($s)];
}, [
$bad_reserved_byte,
@@ -132,8 +246,10 @@ public function invalidRegistrationData(): array {
// -( Helpers )------------------------------------------------------------
- protected function buildJson($clientData, $registrationData): string {
- return sprintf('{"clientData":"%s","registrationData":"%s"}',
+ protected function buildJson(string $clientData, string $registrationData): string
+ {
+ return sprintf(
+ '{"clientData":"%s","registrationData":"%s"}',
$clientData,
$registrationData
);
diff --git a/tests/RegistrationTest.php b/tests/RegistrationTest.php
index 7a73dea..63eaf52 100644
--- a/tests/RegistrationTest.php
+++ b/tests/RegistrationTest.php
@@ -8,27 +8,76 @@
* @covers ::
* @covers ::
*/
-class RegistrationTest extends \PHPUnit_Framework_TestCase
+class RegistrationTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::setCounter
* @covers ::getCounter
*/
- public function testCounter() {
+ public function testCounter(): void
+ {
$obj = new Registration();
- $this->assertSame($obj, $obj->setCounter(1833),
- 'setCounter should return $this');
- $this->assertSame(1833, $obj->getCounter(),
- 'getCounter should return the set value');
+ $this->assertSame(
+ $obj,
+ $obj->setCounter(1833),
+ 'setCounter should return $this'
+ );
+ $this->assertSame(
+ 1833,
+ $obj->getCounter(),
+ 'getCounter should return the set value'
+ );
}
/**
* @covers ::setCounter
*/
- public function testSetCounterRejectsNegativeNumbers() {
+ public function testSetCounterRejectsNegativeNumbers(): void
+ {
$obj = new Registration();
$this->expectException(\OutOfBoundsException::class);
$obj->setCounter(-1);
}
+
+ /**
+ * @covers ::getPublicKey
+ * @covers ::setPublicKey
+ */
+ public function testPublicKey(): void
+ {
+ $pk = $this->createMock(PublicKeyInterface::class);
+ $reg = new Registration();
+ $reg->setPublicKey($pk);
+ $this->assertSame($pk, $reg->getPublicKey());
+ }
+
+ /**
+ * @covers ::getAttestationCertificate
+ * @covers ::setAttestationCertificate
+ */
+ public function testAttestationCertificate(): void
+ {
+ $pk = $this->createMock(AttestationCertificateInterface::class);
+ $reg = new Registration();
+ $reg->setAttestationCertificate($pk);
+ $this->assertSame($pk, $reg->getAttestationCertificate());
+ }
+
+ /**
+ * @covers ::__debugInfo
+ */
+ public function testDebugInfoEncodesBinary(): void
+ {
+ $reg = new Registration();
+ $reg->setAttestationCertificate($this->createMock(AttestationCertificateInterface::class));
+ $reg->setPublicKey($this->createMock(PublicKeyInterface::class));
+ $kh = random_bytes(20);
+ $reg->setKeyHandle($kh);
+ $reg->setCounter(50);
+
+ $debugInfo = $reg->__debugInfo();
+ $this->assertNotSame($kh, $debugInfo['keyHandle']);
+ $this->assertRegExp('/^0x[0-9a-f]{40}$/', $debugInfo['keyHandle']);
+ }
}
diff --git a/tests/ResponseTraitTest.php b/tests/ResponseTraitTest.php
index 7a81aad..903b32d 100644
--- a/tests/ResponseTraitTest.php
+++ b/tests/ResponseTraitTest.php
@@ -8,14 +8,17 @@
* @covers ::
* @covers ::
*/
-class ResponseTraitTest extends \PHPUnit_Framework_TestCase
+class ResponseTraitTest extends \PHPUnit\Framework\TestCase
{
+ /** @var object */
private $trait;
- public function setUp() {
+ public function setUp(): void
+ {
$this->trait = new class {
use ResponseTrait;
- protected function parseResponse(array $response): self {
+ protected function parseResponse(array $response): self
+ {
$this->setSignature($response['signature']);
return $this;
}
@@ -25,12 +28,15 @@ protected function parseResponse(array $response): self {
/**
* @covers ::fromJson
* @covers ::getSignature
- * @covers ::getClientData
*/
- public function testValidJson() {
+ public function testValidJson(): void
+ {
$signature = __METHOD__;
$json = json_encode([
- 'clientData' => 'eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoid3QyemU4SXNrY1RPM25Jc08yRDJoRmpFNXRWRDA0MU5wblllc0xwSndlZyIsIm9yaWdpbiI6Imh0dHBzOi8vdTJmLmVyaWNzdGVybi5jb20iLCJjaWRfcHVia2V5IjoiIn0',
+ 'clientData' => 'eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwi'.
+ 'Y2hhbGxlbmdlIjoid3QyemU4SXNrY1RPM25Jc08yRDJoRmpFNXRWRDA0MU5w'.
+ 'blllc0xwSndlZyIsIm9yaWdpbiI6Imh0dHBzOi8vdTJmLmVyaWNzdGVybi5j'.
+ 'b20iLCJjaWRfcHVia2V5IjoiIn0',
'signature' => $signature,
]);
@@ -38,20 +44,24 @@ public function testValidJson() {
// This is a little goofy because it's an anonymous class, but seems
// preferable to declaring a one-off class in the test to implement the
// trait instead.
- $this->assertInstanceOf(get_class($this->trait), $response,
- 'Parsed response was the wrong type');
+ $this->assertInstanceOf(
+ get_class($this->trait),
+ $response,
+ 'Parsed response was the wrong type'
+ );
- $this->assertInstanceOf(ClientData::class, $response->getClientData(),
- 'ClientData was not parsed correctly');
-
- $this->assertSame(__METHOD__, $response->getSignature(),
- 'Signature was not parsed correctly');
+ $this->assertSame(
+ __METHOD__,
+ $response->getSignature(),
+ 'Signature was not parsed correctly'
+ );
}
/**
* @covers ::fromJson
*/
- public function testFromJsonWithNonJson() {
+ public function testFromJsonWithNonJson(): void
+ {
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
$this->trait::fromJson('this is not json');
@@ -61,7 +71,8 @@ public function testFromJsonWithNonJson() {
* @covers ::fromJson
* @dataProvider clientErrors
*/
- public function testErrorResponse(int $code) {
+ public function testErrorResponse(int $code): void
+ {
$json = sprintf('{"errorCode":%d}', $code);
$this->expectException(ClientErrorException::class);
$this->expectExceptionCode($code);
@@ -72,7 +83,8 @@ public function testErrorResponse(int $code) {
* @covers ::fromJson
* @dataProvider badClientData
*/
- public function testClientDataValidation(string $json, int $code) {
+ public function testClientDataValidation(string $json, int $code): void
+ {
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode($code);
$this->trait::fromJson($json);
@@ -80,7 +92,9 @@ public function testClientDataValidation(string $json, int $code) {
// -( DataProviders )------------------------------------------------------
- public function clientErrors() {
+ /** @return array{int}[] */
+ public function clientErrors(): array
+ {
return [
[ClientError::OTHER_ERROR],
[ClientError::BAD_REQUEST],
@@ -90,11 +104,12 @@ public function clientErrors() {
];
}
- public function badClientData() {
+ /** @return array{string, int}[] */
+ public function badClientData(): array
+ {
return [
['{}', InvalidDataException::MISSING_KEY],
['{"clientData":25}', InvalidDataException::MALFORMED_DATA],
];
}
-
}
diff --git a/tests/SecurityExceptionTest.php b/tests/SecurityExceptionTest.php
index 0ed935b..6e551a8 100644
--- a/tests/SecurityExceptionTest.php
+++ b/tests/SecurityExceptionTest.php
@@ -8,23 +8,30 @@
* @covers ::
* @covers ::
*/
-class SecurityExceptionTest extends \PHPUnit_Framework_TestCase
+class SecurityExceptionTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::__construct
* @dataProvider securityExceptionCodes
*/
- public function testSecurityException(int $code) {
+ public function testSecurityException(int $code): void
+ {
$ex = new SecurityException($code);
- $this->assertInstanceOf(SecurityException::class,
+ $this->assertInstanceOf(
+ SecurityException::class,
$ex,
- '__construct failed');
- $this->assertNotEmpty($ex->getMessage(),
- 'A predefined message should have been used');
+ '__construct failed'
+ );
+ $this->assertNotEmpty(
+ $ex->getMessage(),
+ 'A predefined message should have been used'
+ );
}
// -( DataProviders )------------------------------------------------------
- public function securityExceptionCodes() {
+ /** @return array{int}[] */
+ public function securityExceptionCodes()
+ {
return [
[SecurityException::SIGNATURE_INVALID],
[SecurityException::COUNTER_USED],
@@ -33,5 +40,4 @@ public function securityExceptionCodes() {
[SecurityException::NO_TRUSTED_CA],
];
}
-
}
diff --git a/tests/ServerTest.php b/tests/ServerTest.php
index aeba2cc..4185cdc 100644
--- a/tests/ServerTest.php
+++ b/tests/ServerTest.php
@@ -10,108 +10,180 @@
* @covers ::
* @covers ::
*/
-class ServerTest extends \PHPUnit_Framework_TestCase
+class ServerTest extends \PHPUnit\Framework\TestCase
{
- const APP_ID = 'https://u2f.example.com';
- const ENCODED_KEY_HANDLE = 'JUnVTStPn-V2-bCu0RlvPbukBpHTD5Mi1ZGglDOcN0vD45rnTD0BXdkRt78huTwJ7tVaxTqSetHjr22tCjmYLQ';
- const ENCODED_PUBLIC_KEY = 'BEyIn4ldTViNAgceMA/YgRX1DlJR3bSF39drG44Fx1E2LaF9Md9RUN2CHyfzSokIjjCHP8jMsTYwdt0tKe6qLzc=';
+ const APP_ID = 'https://u2f.ericstern.com';
+ const ENCODED_KEY_HANDLE =
+ 'JUnVTStPn-V2-bCu0RlvPbukBpHTD5Mi1ZGglDOcN0vD45rnTD0BXdkRt78huTwJ7tVax'.
+ 'TqSetHjr22tCjmYLQ';
+
+ const ENCODED_PUBLIC_KEY =
+ 'BEyIn4ldTViNAgceMA/YgRX1DlJR3bSF39drG44Fx1E2LaF9Md9RUN2CHyfzSokIjjCHP'.
+ '8jMsTYwdt0tKe6qLzc=';
+
+ /** @var Server */
private $server;
- public function setUp() {
+ public function setUp(): void
+ {
$this->server = (new Server())
->disableCAVerification()
->setAppId(self::APP_ID);
}
+ /**
+ * @covers ::__construct
+ */
+ public function testConstruct(): void
+ {
+ $server = new Server();
+ $this->assertInstanceOf(Server::class, $server);
+ }
+
/**
* @covers ::disableCAVerification
*/
- public function testDisableCAVerificationReturnsSelf() {
+ public function testDisableCAVerificationReturnsSelf(): void
+ {
$server = new Server();
- $this->assertSame($server, $server->disableCAVerification(),
- 'disableCAVerification did not return $this');
+ $this->assertSame(
+ $server,
+ $server->disableCAVerification(),
+ 'disableCAVerification did not return $this'
+ );
}
/**
* @covers ::generateRegisterRequest
*/
- public function testGenerateRegisterRequest() {
+ public function testGenerateRegisterRequest(): void
+ {
$req = $this->server->generateRegisterRequest();
$this->assertInstanceOf(RegisterRequest::class, $req);
- $this->assertSame(self::APP_ID, $req->getAppId(),
- 'RegisterRequest App ID was not the value from the server');
- $this->assertNotEmpty($req->getChallenge(),
- 'No challenge value was set');
- $this->assertTrue(strlen($req->getChallenge()) >= 8,
- 'Challenge was less than 8 bytes long, violating the spec');
+ $this->assertSame(
+ self::APP_ID,
+ $req->getAppId(),
+ 'RegisterRequest App ID was not the value from the server'
+ );
+ $this->assertNotEmpty(
+ $req->getChallenge(),
+ 'No challenge value was set'
+ );
+ $this->assertTrue(
+ strlen($req->getChallenge()) >= 8,
+ 'Challenge was less than 8 bytes long, violating the spec'
+ );
}
/**
* @covers ::generateSignRequest
*/
- public function testGenerateSignRequest() {
+ public function testGenerateSignRequest(): void
+ {
$kh = \random_bytes(16);
$registration = (new Registration())
->setKeyHandle($kh);
$req = $this->server->generateSignRequest($registration);
$this->assertInstanceOf(SignRequest::class, $req);
- $this->assertSame($kh, $req->getKeyHandleBinary(),
- 'Key handle was not set correctly');
- $this->assertSame(self::APP_ID, $req->getAppId(),
- 'SignRequest App ID was not the value form the server');
- $this->assertNotEmpty($req->getChallenge(),
- 'No challenge value was set');
- $this->assertTrue(strlen($req->getChallenge()) >= 8,
- 'Challenge was less than 8 bytes long, violating the spec');
+ $this->assertSame(
+ $kh,
+ $req->getKeyHandleBinary(),
+ 'Key handle was not set correctly'
+ );
+ $this->assertSame(
+ self::APP_ID,
+ $req->getAppId(),
+ 'SignRequest App ID was not the value form the server'
+ );
+ $this->assertNotEmpty(
+ $req->getChallenge(),
+ 'No challenge value was set'
+ );
+ $this->assertTrue(
+ strlen($req->getChallenge()) >= 8,
+ 'Challenge was less than 8 bytes long, violating the spec'
+ );
+ }
+
+ /**
+ * @covers ::generateSignRequests
+ */
+ public function testGenerateSignRequests(): void
+ {
+ $registrations = [
+ (new Registration())->setKeyHandle(\random_bytes(16)),
+ (new Registration())->setKeyHandle(\random_bytes(16)),
+ ];
+ $signRequests = $this->server->generateSignRequests($registrations);
+
+ $this->assertIsArray($signRequests);
+ foreach ($signRequests as $signRequest) {
+ $this->assertInstanceOf(SignRequest::class, $signRequest);
+ }
+ // This method is a simple map operation, so testGenerateSignRequest
+ // does the heavy lifting.
}
/**
* @covers ::setRegisterRequest
*/
- public function testSetRegisterRequestReturnsSelf() {
+ public function testSetRegisterRequestReturnsSelf(): void
+ {
$req = $this->getDefaultRegisterRequest();
- $this->assertSame($this->server,
+ $this->assertSame(
+ $this->server,
$this->server->setRegisterRequest($req),
- 'setRegisterRequest did not return $this');
+ 'setRegisterRequest did not return $this'
+ );
}
/**
* @covers ::setRegistrations
*/
- public function testSetRegistrationsReturnsSelf() {
+ public function testSetRegistrationsReturnsSelf(): void
+ {
$reg = $this->getDefaultRegistration();
- $this->assertSame($this->server,
+ $this->assertSame(
+ $this->server,
$this->server->setRegistrations([$reg]),
- 'setRegistrations did not return $this');
+ 'setRegistrations did not return $this'
+ );
}
/**
* @covers ::setRegistrations
*/
- public function testSetRegistrationsEnforcesTypeCheck() {
+ public function testSetRegistrationsEnforcesTypeCheck(): void
+ {
$wrong = true;
$this->expectException(TypeError::class);
+ // @phpstan-ignore-next-line
$this->server->setRegistrations([$wrong]);
}
/**
* @covers ::setSignRequests
*/
- public function testSetSignRequestsReturnsSelf() {
+ public function testSetSignRequestsReturnsSelf(): void
+ {
$req = $this->getDefaultSignRequest();
- $this->assertSame($this->server,
+ $this->assertSame(
+ $this->server,
$this->server->setSignRequests([$req]),
- 'setSignRequests did not return $this');
+ 'setSignRequests did not return $this'
+ );
}
/**
* @covers ::setSignRequests
*/
- public function testSetSignRequestsEnforcesTypeCheck() {
+ public function testSetSignRequestsEnforcesTypeCheck(): void
+ {
$wrong = true;
$this->expectException(TypeError::class);
+ // @phpstan-ignore-next-line
$this->server->setSignRequests([$wrong]);
}
@@ -120,7 +192,8 @@ public function testSetSignRequestsEnforcesTypeCheck() {
/**
* @covers ::register
*/
- public function testRegisterThrowsIfNoRegistrationRequestProvided() {
+ public function testRegisterThrowsIfNoRegistrationRequestProvided(): void
+ {
$this->expectException(BadMethodCallException::class);
$this->server->register($this->getDefaultRegisterResponse());
}
@@ -128,35 +201,49 @@ public function testRegisterThrowsIfNoRegistrationRequestProvided() {
/**
* @covers ::register
*/
- public function testRegistration() {
+ public function testRegistration(): void
+ {
$request = $this->getDefaultRegisterRequest();
$response = $this->getDefaultRegisterResponse();
$registration = $this->server
->setRegisterRequest($request)
->register($response);
- $this->assertInstanceOf(Registration::class, $registration,
- 'Server->register did not return a registration');
- $this->assertSame(0, $registration->getCounter(),
- 'Counter should start at 0');
-
- $this->assertSame($response->getAttestationCertificateBinary(),
- $registration->getAttestationCertificateBinary(),
- 'Attestation cert was not copied from response');
-
- $this->assertSame($response->getKeyHandleBinary(),
+ $this->assertInstanceOf(
+ RegistrationInterface::class,
+ $registration,
+ 'Server->register did not return a registration'
+ );
+ $this->assertSame(
+ 0,
+ $registration->getCounter(),
+ 'Counter should start at 0'
+ );
+
+ $this->assertSame(
+ $response->getAttestationCertificate()->getBinary(),
+ $registration->getAttestationCertificate()->getBinary(),
+ 'Attestation cert was not copied from response'
+ );
+
+ $this->assertSame(
+ $response->getKeyHandleBinary(),
$registration->getKeyHandleBinary(),
- 'Key handle was not copied from response');
+ 'Key handle was not copied from response'
+ );
- $this->assertSame($response->getPublicKey(),
- $registration->getPublicKey(),
- 'Public key was not copied from response');
+ $this->assertSame(
+ $response->getPublicKey()->getBinary(),
+ $registration->getPublicKey()->getBinary(),
+ 'Public key was not copied from response'
+ );
}
/**
* @covers ::register
*/
- public function testRegisterDefaultsToTryingEmptyCAList() {
+ public function testRegisterDefaultsToTryingEmptyCAList(): void
+ {
$request = $this->getDefaultRegisterRequest();
$response = $this->getDefaultRegisterResponse();
@@ -174,7 +261,8 @@ public function testRegisterDefaultsToTryingEmptyCAList() {
/**
* @covers ::register
*/
- public function testRegisterThrowsIfChallengeDoesNotMatch() {
+ public function testRegisterThrowsIfChallengeDoesNotMatch(): void
+ {
// This would have come from a session, database, etc.
$request = (new RegisterRequest())
->setAppId('https://u2f.ericstern.com')
@@ -191,7 +279,8 @@ public function testRegisterThrowsIfChallengeDoesNotMatch() {
/**
* @covers ::register
*/
- public function testRegisterThrowsWithUntrustedDeviceIssuerCertificate() {
+ public function testRegisterThrowsWithUntrustedDeviceIssuerCertificate(): void
+ {
$request = $this->getDefaultRegisterRequest();
$response = $this->getDefaultRegisterResponse();
@@ -211,7 +300,8 @@ public function testRegisterThrowsWithUntrustedDeviceIssuerCertificate() {
* @covers ::register
* @covers ::setTrustedCAs
*/
- public function testRegisterWorksWithCAList() {
+ public function testRegisterWorksWithCAList(): void
+ {
$request = $this->getDefaultRegisterRequest();
$response = $this->getDefaultRegisterResponse();
// This contains the actual trusted + verified certificates which are
@@ -219,10 +309,11 @@ public function testRegisterWorksWithCAList() {
// generated with a YubiCo device and separately tested against
// a different reference implementation.
$CAs = glob(dirname(__DIR__).'/CAcerts/*.pem');
+ assert($CAs !== false);
$this->server->setTrustedCAs($CAs);
try {
- $this->server
+ $reg = $this->server
->setRegisterRequest($request)
->register($response);
} catch (SecurityException $e) {
@@ -231,21 +322,24 @@ public function testRegisterWorksWithCAList() {
}
throw $e;
}
- // Implicit pass - no exceptions should be thrown at all
+ $this->assertInstanceOf(RegistrationInterface::class, $reg);
}
/**
* @covers ::register
*/
- public function testRegisterThrowsWithChangedApplicationParameter() {
- $request = (new RegisterRequest())
- ->setAppId('https://not.my.u2f.example.com')
- ->setChallenge('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo');
+ public function testRegisterThrowsWithChangedApplicationParameter(): void
+ {
+ $request = $this->getDefaultRegisterRequest();
- $response = $this->getDefaultRegisterResponse();
+ $response = $this->createMock(RegistrationResponseInterface::class);
+ $response->method('getChallenge')
+ ->willReturn($request->getChallenge());
+ $response->method('getRpIdHash')
+ ->willReturn(hash('sha256', 'https://some.otherdomain.com', true));
$this->expectException(SecurityException::class);
- $this->expectExceptionCode(SecurityException::SIGNATURE_INVALID);
+ $this->expectExceptionCode(SecurityException::WRONG_RELYING_PARTY);
$this->server
->setRegisterRequest($request)
->register($response);
@@ -254,17 +348,17 @@ public function testRegisterThrowsWithChangedApplicationParameter() {
/**
* @covers ::register
*/
- public function testRegisterThrowsWithChangedChallengeParameter() {
+ public function testRegisterThrowsWithChangedChallengeParameter(): void
+ {
$request = $this->getDefaultRegisterRequest();
// Mess up some known-good data: challenge parameter
- $json = file_get_contents(__DIR__.'/register_response.json');
- $data = json_decode($json, true);
+ $data = $this->readJsonFile('register_response.json');
$cli = fromBase64Web($data['clientData']);
$obj = json_decode($cli, true);
- $obj['origin'] = 'https://not.my.u2f.example.com';
- $cli = toBase64Web(json_encode($obj));
+ $obj['cid_pubkey'] = 'nonsense';
+ $cli = toBase64Web($this->safeEncode($obj));
$data['clientData'] = $cli;
- $response = RegisterResponse::fromJson(json_encode($data));
+ $response = RegisterResponse::fromJson($this->safeEncode($data));
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::SIGNATURE_INVALID);
@@ -276,15 +370,15 @@ public function testRegisterThrowsWithChangedChallengeParameter() {
/**
* @covers ::register
*/
- public function testRegisterThrowsWithChangedKeyHandle() {
+ public function testRegisterThrowsWithChangedKeyHandle(): void
+ {
$request = $this->getDefaultRegisterRequest();
// Mess up some known-good data: key handle
- $json = file_get_contents(__DIR__.'/register_response.json');
- $data = json_decode($json, true);
+ $data = $this->readJsonFile('register_response.json');
$reg = $data['registrationData'];
- $reg[70] = $reg[70] ^ 0xFF; // Invert a byte in the key handle
+ $reg[70] = chr(ord($reg[70]) + 1); // Change a byte in the key handle
$data['registrationData'] = $reg;
- $response = RegisterResponse::fromJson(json_encode($data));
+ $response = RegisterResponse::fromJson($this->safeEncode($data));
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::SIGNATURE_INVALID);
@@ -296,15 +390,15 @@ public function testRegisterThrowsWithChangedKeyHandle() {
/**
* @covers ::register
*/
- public function testRegisterThrowsWithChangedPubkey() {
+ public function testRegisterThrowsWithChangedPubkey(): void
+ {
$request = $this->getDefaultRegisterRequest();
// Mess up some known-good data: public key
- $json = file_get_contents(__DIR__.'/register_response.json');
- $data = json_decode($json, true);
+ $data = $this->readJsonFile('register_response.json');
$reg = $data['registrationData'];
- $reg[3] = $reg[3] ^ 0xFF; // Invert a byte in the public key
+ $reg[3] = chr(ord($reg[3]) + 1); // Change a byte in the public key
$data['registrationData'] = $reg;
- $response = RegisterResponse::fromJson(json_encode($data));
+ $response = RegisterResponse::fromJson($this->safeEncode($data));
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::SIGNATURE_INVALID);
@@ -316,15 +410,15 @@ public function testRegisterThrowsWithChangedPubkey() {
/**
* @covers ::register
*/
- public function testRegisterThrowsWithBadSignature() {
+ public function testRegisterThrowsWithBadSignature(): void
+ {
$request = $this->getDefaultRegisterRequest();
// Mess up some known-good data: signature
- $json = file_get_contents(__DIR__.'/register_response.json');
- $data = json_decode($json, true);
+ $data = $this->readJsonFile('register_response.json');
$reg = $data['registrationData'];
$last = str_rot13(substr($reg, -5)); // rot13 a few chars in signature
- $data['registrationData'] = substr($reg,0,-5).$last;
- $response = RegisterResponse::fromJson(json_encode($data));
+ $data['registrationData'] = substr($reg, 0, -5).$last;
+ $response = RegisterResponse::fromJson($this->safeEncode($data));
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::SIGNATURE_INVALID);
@@ -338,7 +432,8 @@ public function testRegisterThrowsWithBadSignature() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsIfNoRegistrationsPresent() {
+ public function testAuthenticateThrowsIfNoRegistrationsPresent(): void
+ {
$this->server->setSignRequests([$this->getDefaultSignRequest()]);
$this->expectException(BadMethodCallException::class);
$this->server->authenticate($this->getDefaultSignResponse());
@@ -347,7 +442,8 @@ public function testAuthenticateThrowsIfNoRegistrationsPresent() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsIfNoSignRequestsPresent() {
+ public function testAuthenticateThrowsIfNoSignRequestsPresent(): void
+ {
$this->server->setRegistrations([$this->getDefaultRegistration()]);
$this->expectException(BadMethodCallException::class);
$this->server->authenticate($this->getDefaultSignResponse());
@@ -356,7 +452,8 @@ public function testAuthenticateThrowsIfNoSignRequestsPresent() {
/**
* @covers ::authenticate
*/
- public function testAuthenticate() {
+ public function testAuthenticate(): void
+ {
// All normal
$registration = $this->getDefaultRegistration();
$request = $this->getDefaultSignRequest();
@@ -366,33 +463,23 @@ public function testAuthenticate() {
->setRegistrations([$registration])
->setSignRequests([$request])
->authenticate($response);
- $this->assertInstanceOf(Registration::class, $return,
- 'A successful authentication should have returned a Registration');
- $this->assertNotSame($registration, $return,
- 'A new instance of Registration should have been returned');
- $this->assertSame($response->getCounter(), $return->getCounter(),
- 'The new Registration\'s counter did not match the Response');
- }
-
- /**
- * Re-run testAuthenticate after ensuring that mbstring.func_overload is
- * being used
- *
- * @coversNothing
- */
- public function testAuthenticateWithMultibyteSettings()
- {
- $overload = ini_get('mbstring.func_overload');
- if ($overload != 7) {
- $cmd = 'php -d mbstring.func_overload=7 '.
- implode(' ', $_SERVER['argv']);
- $this->markTestSkipped(sprintf(
- "mbstring.func_overload cannot be changed at runtime. Re-run ".
- "this test with the following command:\n\n%s",
- $cmd));
- }
-
- $this->testAuthenticate();
+ $this->assertInstanceOf(
+ RegistrationInterface::class,
+ $return,
+ 'A successful authentication should have returned an object '.
+ 'implementing RegistrationInterface'
+ );
+ $this->assertNotSame(
+ $registration,
+ $return,
+ 'A new object implementing RegistrationInterface should have been '.
+ 'returned'
+ );
+ $this->assertSame(
+ $response->getCounter(),
+ $return->getCounter(),
+ 'The new registration\'s counter did not match the Response'
+ );
}
/**
@@ -401,7 +488,8 @@ public function testAuthenticateWithMultibyteSettings()
*
* @covers ::authenticate
*/
- public function testAuthenticateThrowsWithObviousReplayAttack() {
+ public function testAuthenticateThrowsWithObviousReplayAttack(): void
+ {
// All normal
$registration = $this->getDefaultRegistration();
$request = $this->getDefaultSignRequest();
@@ -428,11 +516,12 @@ public function testAuthenticateThrowsWithObviousReplayAttack() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsWhenCounterGoesBackwards() {
+ public function testAuthenticateThrowsWhenCounterGoesBackwards(): void
+ {
// Counter from "DB" bumped, suggesting response was cloned
$registration = (new Registration())
->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE))
- ->setPublicKey(base64_decode(self::ENCODED_PUBLIC_KEY))
+ ->setPublicKey($this->getDefaultPublicKey())
->setCounter(82)
;
$request = $this->getDefaultSignRequest();
@@ -449,7 +538,8 @@ public function testAuthenticateThrowsWhenCounterGoesBackwards() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsWhenChallengeDoesNotMatch() {
+ public function testAuthenticateThrowsWhenChallengeDoesNotMatch(): void
+ {
$registration = $this->getDefaultRegistration();
// Change request challenge
$request = (new SignRequest())
@@ -470,11 +560,12 @@ public function testAuthenticateThrowsWhenChallengeDoesNotMatch() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsIfNoRegistrationMatchesKeyHandle() {
+ public function testAuthenticateThrowsIfNoRegistrationMatchesKeyHandle(): void
+ {
// Change registration KH
$registration = (new Registration())
->setKeyHandle(fromBase64Web('some-other-key-handle'))
- ->setPublicKey(base64_decode(self::ENCODED_PUBLIC_KEY))
+ ->setPublicKey($this->getDefaultPublicKey())
->setCounter(2)
;
$request = $this->getDefaultSignRequest();
@@ -491,7 +582,8 @@ public function testAuthenticateThrowsIfNoRegistrationMatchesKeyHandle() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsIfNoRequestMatchesKeyHandle() {
+ public function testAuthenticateThrowsIfNoRequestMatchesKeyHandle(): void
+ {
$registration = $this->getDefaultRegistration();
// Change request KH
$request = (new SignRequest())
@@ -512,15 +604,14 @@ public function testAuthenticateThrowsIfNoRequestMatchesKeyHandle() {
/**
* @covers ::authenticate
*/
- public function testAuthenticateThrowsIfSignatureIsInvalid() {
+ public function testAuthenticateThrowsIfSignatureIsInvalid(): void
+ {
$registration = $this->getDefaultRegistration();
$request = $this->getDefaultSignRequest();
// Trimming a byte off the signature to cause a mismatch
- $data = json_decode(
- file_get_contents(__DIR__.'/sign_response.json'),
- true);
+ $data = $this->readJsonFile('sign_response.json');
$data['signatureData'] = substr($data['signatureData'], 0, -1);
- $response = SignResponse::fromJson(json_encode($data));
+ $response = SignResponse::fromJson($this->safeEncode($data));
$this->expectException(SecurityException::class);
$this->expectExceptionCode(SecurityException::SIGNATURE_INVALID);
@@ -537,7 +628,13 @@ public function testAuthenticateThrowsIfSignatureIsInvalid() {
*
* @covers ::authenticate
*/
- public function testAuthenticateThrowsIfRequestIsSignedWithWrongKey() {
+ public function testAuthenticateThrowsIfRequestIsSignedWithWrongKey(): void
+ {
+ $pk = base64_decode(
+ 'BCXk9bGiuzLRJaX6pFONm+twgIrDkOSNDdXgltt+KhOD'.
+ '9OxeRv2zYiz7SrVa8eb4LbGR9IDUE7gJySiiuQYWt1w='
+ );
+ assert($pk !== false);
// This was a different key genearated with:
// $ openssl ecparam -name prime256v1 -genkey -out private.pem
// $ openssl ec -in private.pem -pubout -out public.pem
@@ -545,9 +642,7 @@ public function testAuthenticateThrowsIfRequestIsSignedWithWrongKey() {
// leading bytes are formatting; see ECPublicKeyTrait)
$registration = (new Registration())
->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE))
- ->setPublicKey(base64_decode(
- 'BCXk9bGiuzLRJaX6pFONm+twgIrDkOSNDdXgltt+KhOD'.
- '9OxeRv2zYiz7SrVa8eb4LbGR9IDUE7gJySiiuQYWt1w='))
+ ->setPublicKey(new ECPublicKey($pk))
->setCounter(2)
;
$request = $this->getDefaultSignRequest();
@@ -560,21 +655,89 @@ public function testAuthenticateThrowsIfRequestIsSignedWithWrongKey() {
->authenticate($response);
}
+ // -( Alternate formats (see #14) )----------------------------------------
+
+ public function testRegistrationWithoutCidPubkeyBug14Case1(): void
+ {
+ $server = (new Server())
+ ->disableCAVerification()
+ ->setAppId('https://u2f.ericstern.com');
+
+ $registerRequest = new RegisterRequest();
+ $registerRequest->setAppId($server->getAppId())
+ ->setChallenge('dNqjowssvlxx9zBhvsy03A');
+ $server->setRegisterRequest($registerRequest);
+
+ $json = '{"registrationData":"BQSFDYsZaHlRBQcdLyu4jZ-Bukb1vw6QtSfmvTQO'.
+ 'IXpjZpfqYptdtpBznuNBslzlZdodspfqRkqwJIt3a0W2P_HlQImHG1FoSkYdPwSzp'.
+ '3WvlDisShW5fveiaaI4Zk8oZBkyWoQ6v1c2ypcd5OWPX6rAH-N7cPjw1Vg_w1q_YL'.
+ 'c3mR8wggE0MIHboAMCAQICCjJ1rwmwx867ew8wCgYIKoZIzj0EAwIwFTETMBEGA1U'.
+ 'EAxMKVTJGIElzc3VlcjAaFwswMDAxMDEwMDAwWhcLMDAwMTAxMDAwMFowFTETMBEG'.
+ 'A1UEAxMKVTJGIERldmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCLzJT4vt'.
+ 'kl-799Ks5wINHdVRIKCLq-kX6oIajh_2Dv4Sk0cBVteQt1xdGau1XzEaGYIOvU5hU'.
+ 'm2J2pxVBQIzaajFzAVMBMGCysGAQQBguUcAgEBBAQDAgUgMAoGCCqGSM49BAMCA0g'.
+ 'AMEUCIQDBo6aOLxanIUYnBX9iu3KMngPnobpi0EZSTkVtLC8_cwIgC1945RGqGBKf'.
+ 'byNtkhMifZK05n7fU-gW37Bdnci5D94wRQIgEPJVWZ7zgVQUctG3xpWBv77s3u2R7'.
+ 'OJP-UjkWdcUs2QCIQC1fqlZIrl4kIEsSQTRMauvcaoeunV-I24WYnp3rgC_Dg","v'.
+ 'ersion":"U2F_V2","challenge":"dNqjowssvlxx9zBhvsy03A","appId":"ht'.
+ 'tps://u2f.ericstern.com","clientData":"eyJjaGFsbGVuZ2UiOiJkTnFqb3'.
+ 'dzc3ZseHg5ekJodnN5MDNBIiwib3JpZ2luIjoiaHR0cHM6Ly91MmYuZXJpY3N0ZXJ'.
+ 'uLmNvbSIsInR5cCI6Im5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50In0"}';
+ $registerResponse = RegisterResponse::fromJson($json);
+
+ $registration = $server->register($registerResponse);
+ $this->assertInstanceOf(Registration::class, $registration);
+ }
+
+ public function testRegistrationWithoutCidPubkeyBug14Case2(): void
+ {
+ $server = (new Server())
+ ->disableCAVerification()
+ ->setAppId('https://u2f.ericstern.com');
+
+ $registerRequest = new RegisterRequest();
+ $registerRequest->setAppId($server->getAppId())
+ ->setChallenge('E23usdC7VkxjN1mwRAeyjg');
+ $server->setRegisterRequest($registerRequest);
+
+ $json = '{"registrationData":"BQSTffB-e9hdFwhsfb2t-2ppwyxZAltnDf6TYwv4'.
+ '1VtleEO4488JwNFGr_bks_4EzA4DoluDBCgfmULGpZpXykTZQMOMz9DfbESHnuBY9'.
+ 'cmTxVTVtrsTFTQA-IPETCYJ2dYACULXRN7_qLq_2WnDQJaME7zWyZEB0NFu-hosav'.
+ 'uqjncwggEbMIHCoAMCAQICCiIygbKxS2KpYY8wCgYIKoZIzj0EAwIwFTETMBEGA1U'.
+ 'EAxMKVTJGIElzc3VlcjAaFwswMDAxMDEwMDAwWhcLMDAwMTAxMDAwMFowFTETMBEG'.
+ 'A1UEAxMKVTJGIERldmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCdqjfpHR'.
+ '9L8a6-pVRv9PWu-pORC9sO9eDk6ZlFIXaclyfxbLJqAehvIWJuzij_BxJOLbQPD_9'.
+ 'fX5uKh9tDv8nowCgYIKoZIzj0EAwIDSAAwRQIhAMGjpo4vFqchRicFf2K7coyeA-e'.
+ 'humLQRlJORW0sLz9zAiALX3jlEaoYEp9vI22SEyJ9krTmft9T6BbfsF2dyLkP3jBE'.
+ 'AiAHD70-wA4f3SZk6s0RocHAA4nDCGaVFvTBG4gZXcZTnQIge2joenpQxVP0r1o9E'.
+ 'zL9C3aR-HEKhSHr86MX4eUTMlw","version":"U2F_V2","challenge":"E23us'.
+ 'dC7VkxjN1mwRAeyjg","appId":"https://u2f.ericstern.com","clientDat'.
+ 'a":"eyJjaGFsbGVuZ2UiOiJFMjN1c2RDN1ZreGpOMW13UkFleWpnIiwib3JpZ2luI'.
+ 'joiaHR0cHM6Ly91MmYuZXJpY3N0ZXJuLmNvbSIsInR5cCI6Im5hdmlnYXRvci5pZC'.
+ '5maW5pc2hFbnJvbGxtZW50In0"}';
+ $registerResponse = RegisterResponse::fromJson($json);
+
+ $registration = $server->register($registerResponse);
+ $this->assertInstanceOf(Registration::class, $registration);
+ }
+
// -( Helpers )------------------------------------------------------------
- private function getDefaultRegisterRequest(): RegisterRequest {
+ private function getDefaultRegisterRequest(): RegisterRequest
+ {
// This would have come from a session, database, etc.
return (new RegisterRequest())
->setAppId('https://u2f.ericstern.com')
->setChallenge('PfsWR1Umy2V5Al1Bam2tG0yfPLeJElfwRzzAzkYPgzo');
}
- private function getDefaultRegisterResponse(): RegisterResponse {
- return RegisterResponse::fromJson(
- file_get_contents(__DIR__.'/register_response.json'));
+ private function getDefaultRegisterResponse(): RegisterResponse
+ {
+ return RegisterResponse::fromJson($this->safeReadFile('register_response.json'));
}
- private function getDefaultSignRequest(): SignRequest {
+ private function getDefaultSignRequest(): SignRequest
+ {
// This would have come from a session, database, etc
return (new SignRequest())
->setAppId('https://u2f.ericstern.com')
@@ -583,18 +746,82 @@ private function getDefaultSignRequest(): SignRequest {
;
}
- private function getDefaultRegistration(): Registration {
+ private function getDefaultRegistration(): RegistrationInterface
+ {
// From database attached to the authenticating user
return (new Registration())
->setKeyHandle(fromBase64Web(self::ENCODED_KEY_HANDLE))
- ->setPublicKey(base64_decode(self::ENCODED_PUBLIC_KEY))
+ ->setAttestationCertificate($this->getDefaultAttestationCertificate())
+ ->setPublicKey($this->getDefaultPublicKey())
->setCounter(2)
;
}
- private function getDefaultSignResponse(): SignResponse {
+ private function getDefaultSignResponse(): SignResponse
+ {
// Value from user
- return SignResponse::fromJson(
- file_get_contents(__DIR__.'/sign_response.json'));
+ return SignResponse::fromJson($this->safeReadFile('sign_response.json'));
+ }
+
+ private function getDefaultAttestationCertificate(): AttestationCertificate
+ {
+ $attest = hex2bin(
+ '3082022d30820117a003020102020405b60579300b06092a864886f70d01010b'.
+ '302e312c302a0603550403132359756269636f2055324620526f6f7420434120'.
+ '53657269616c203435373230303633313020170d313430383031303030303030'.
+ '5a180f32303530303930343030303030305a30283126302406035504030c1d59'.
+ '756269636f205532462045452053657269616c20393538313530333330593013'.
+ '06072a8648ce3d020106082a8648ce3d03010703420004fdb8deb3a1ed70eb63'.
+ '6c066eb6006996a5f970fcb5db88fc3b305d41e5966f0c1b54b852fef0a0907e'.
+ 'd17f3bffc29d4d321b9cf8a84a2ceaa038cabd35d598dea3263024302206092b'.
+ '0601040182c40a020415312e332e362e312e342e312e34313438322e312e3130'.
+ '0b06092a864886f70d01010b03820101007ed3fb6ccc252013f82f218c2a37da'.
+ '6031d20e7f3081dafcaeb128fc7f9b233914bfb64d6135f17ce221fa764f453e'.
+ 'f1273a8ce965956442bb2f1e47483f737dcbc98b585377fef50b270e0289f884'.
+ '36f1adcf49b2621ee5e302df555b9ab74272e069f918149b3dec4f12228b10c0'.
+ 'f88de36af58a74bb442b85ae005364bda6702058fc1f2d879b530111ea60e86c'.
+ '63f17fa5944cc83f0aa269848b3ee388a6c09e6b05953fcbb8f47e83a27e0072'.
+ 'a63c32ad64864e926d7112fa1997f7839656fbb32be8f7889d0f0145519a27af'.
+ 'dd8e46b04ca4290d8540b634b886161e7588c86299dcdd6435d1678a3a6f0a74'.
+ '829c4dd3f70c3524d1ddf16d78add21b64'
+ );
+ assert($attest !== false);
+ return new AttestationCertificate($attest);
+ }
+
+ public function getDefaultPublicKey(): PublicKeyInterface
+ {
+ $pk = base64_decode(self::ENCODED_PUBLIC_KEY);
+ assert($pk !== false);
+ return new ECPublicKey($pk);
+ }
+
+ /** @return mixed[] */
+ private function readJsonFile(string $file): array
+ {
+ return $this->safeDecode($this->safeReadFile($file));
+ }
+
+ private function safeReadFile(string $file): string
+ {
+ $body = file_get_contents(__DIR__.'/'.$file);
+ assert($body !== false);
+ return $body;
+ }
+
+ /** @return mixed[] */
+ private function safeDecode(string $json): array
+ {
+ $data = json_decode($json, true);
+ assert($data !== false);
+ return $data;
+ }
+
+ /** @param mixed[] $data */
+ private function safeEncode(array $data): string
+ {
+ $json = json_encode($data);
+ assert($json !== false);
+ return $json;
}
}
diff --git a/tests/SignRequestTest.php b/tests/SignRequestTest.php
index 1959c75..c2997c2 100644
--- a/tests/SignRequestTest.php
+++ b/tests/SignRequestTest.php
@@ -8,12 +8,13 @@
* @covers ::
* @covers ::
*/
-class SignRequestTest extends \PHPUnit_Framework_TestCase
+class SignRequestTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::jsonSerialize
*/
- public function testJsonSerialize() {
+ public function testJsonSerialize(): void
+ {
$appId = 'https://u2f.example.com';
$challenge = 'some-random-string';
$keyHandle = random_bytes(20);
@@ -22,30 +23,57 @@ public function testJsonSerialize() {
$request
->setAppId($appId)
->setChallenge($challenge)
- ->setKeyHandle($keyHandle);;
+ ->setKeyHandle($keyHandle);
$json = json_encode($request);
+ assert($json !== false);
$decoded = json_decode($json, true);
- $this->assertSame($appId, $request->getAppId(),
- 'getAppId returned the wrong value');
- $this->assertSame($appId, $decoded['appId'],
- 'json appId property did not match');
+ $this->assertSame(
+ $appId,
+ $request->getAppId(),
+ 'getAppId returned the wrong value'
+ );
+ $this->assertSame(
+ $appId,
+ $decoded['appId'],
+ 'json appId property did not match'
+ );
- $this->assertSame($challenge, $request->getChallenge(),
- 'getChallenge returned the wrong value');
- $this->assertSame($challenge, $decoded['challenge'],
- 'json challenge property did not match');
+ $this->assertSame(
+ $challenge,
+ $request->getChallenge(),
+ 'getChallenge returned the wrong value'
+ );
+ $this->assertSame(
+ $challenge,
+ $decoded['challenge'],
+ 'json challenge property did not match'
+ );
- $this->assertSame($keyHandle, $request->getKeyHandleBinary(),
- 'getKeyHandleBinary returned the wrong value');
- $this->assertSame(toBase64Web($keyHandle), $request->getKeyHandleWeb(),
- 'getKeyHandleWeb returned the wrong value');
- $this->assertSame(toBase64Web($keyHandle), $decoded['keyHandle'],
- 'json keyHandle property did not match');
+ $this->assertSame(
+ $keyHandle,
+ $request->getKeyHandleBinary(),
+ 'getKeyHandleBinary returned the wrong value'
+ );
+ $this->assertSame(
+ toBase64Web($keyHandle),
+ $request->getKeyHandleWeb(),
+ 'getKeyHandleWeb returned the wrong value'
+ );
+ $this->assertSame(
+ toBase64Web($keyHandle),
+ $decoded['keyHandle'],
+ 'json keyHandle property did not match'
+ );
- $this->assertSame('U2F_V2', $request->getVersion(),
- 'getVersion returned the wrong value');
- $this->assertSame('U2F_V2', $decoded['version'],
- 'json version was incorrect');
+ $this->assertSame(
+ 'U2F_V2',
+ $request->getVersion(),
+ 'getVersion returned the wrong value'
+ );
+ $this->assertSame(
+ 'U2F_V2',
+ $decoded['version'],
+ 'json version was incorrect'
+ );
}
-
}
diff --git a/tests/SignResponseTest.php b/tests/SignResponseTest.php
index cee3fb8..58f0839 100644
--- a/tests/SignResponseTest.php
+++ b/tests/SignResponseTest.php
@@ -8,22 +8,38 @@
* @covers ::
* @covers ::
*/
-class SignResponseTest extends \PHPUnit_Framework_TestCase
+class SignResponseTest extends \PHPUnit\Framework\TestCase
{
const JSON_FORMAT = '{"keyHandle":"%s","clientData":"%s","signatureData":"%s"}';
- private $valid_key_handle = 'JUnVTStPn-V2-bCu0RlvPbukBpHTD5Mi1ZGglDOcN0vD45rnTD0BXdkRt78huTwJ7tVaxTqSetHjr22tCjmYLQ';
- private $valid_client_data = 'eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoid3QyemU4SXNrY1RPM25Jc08yRDJoRmpFNXRWRDA0MU5wblllc0xwSndlZyIsIm9yaWdpbiI6Imh0dHBzOi8vdTJmLmVyaWNzdGVybi5jb20iLCJjaWRfcHVia2V5IjoiIn0';
- private $valid_signature_data = 'AQAAAC0wRgIhAJPy1RvD1WCw1XZX53BXydX_Kyf_XZQueFSIPigRF-D2AiEAx3bJr5ixrXGdUX1XooAfhz15ZIY8rC5H4qaW7gQspJ4';
+
+ /** @var string */
+ private $valid_key_handle =
+ 'JUnVTStPn-V2-bCu0RlvPbukBpHTD5Mi1ZGglDOcN0vD45rnTD0BXdkRt78huTwJ7tVax'.
+ 'TqSetHjr22tCjmYLQ';
+
+ /** @var string */
+ private $valid_client_data =
+ 'eyJ0eXAiOiJuYXZpZ2F0b3IuaWQuZ2V0QXNzZXJ0aW9uIiwiY2hhbGxlbmdlIjoid3Qye'.
+ 'mU4SXNrY1RPM25Jc08yRDJoRmpFNXRWRDA0MU5wblllc0xwSndlZyIsIm9yaWdpbiI6Im'.
+ 'h0dHBzOi8vdTJmLmVyaWNzdGVybi5jb20iLCJjaWRfcHVia2V5IjoiIn0';
+
+ /** @var string */
+ private $valid_signature_data =
+ 'AQAAAC0wRgIhAJPy1RvD1WCw1XZX53BXydX_Kyf_XZQueFSIPigRF-D2AiEAx3bJr5ixr'.
+ 'XGdUX1XooAfhz15ZIY8rC5H4qaW7gQspJ4';
/**
* @covers ::fromJson
*/
- public function testFromJsonWorks() {
- $json = sprintf(self::JSON_FORMAT,
+ public function testFromJsonWorks(): void
+ {
+ $json = sprintf(
+ self::JSON_FORMAT,
$this->valid_key_handle,
$this->valid_client_data,
- $this->valid_signature_data);
+ $this->valid_signature_data
+ );
$response = SignResponse::fromJson($json);
$this->assertInstanceOf(SignResponse::class, $response);
}
@@ -33,107 +49,202 @@ public function testFromJsonWorks() {
* @covers ::getSignature
* @covers ::getUserPresenceByte
*/
- public function testDataAccuracyAfterSuccessfulParsing() {
+ public function testDataAccuracyAfterSuccessfulParsing(): void
+ {
$sig = random_bytes(16);
- $counter = random_int(0, pow(2,32));
+ $counter = random_int(0, pow(2, 32));
$signature_data = toBase64Web(pack('CNA*', 1, $counter, $sig));
$challenge = toBase64Web(random_bytes(32));
$key_handle = toBase64Web(random_bytes(16));
- $client_data = toBase64Web(json_encode([
+ $json = json_encode([
"typ" => "navigator.id.getAssertion",
"challenge" => $challenge,
"origin" => "https://u2f.example.com",
"cid_pubkey" => ""
- ]));
+ ]);
+ assert($json !== false);
+ $client_data = toBase64Web($json);
- $json = sprintf(self::JSON_FORMAT,
+ $json = sprintf(
+ self::JSON_FORMAT,
$key_handle,
$client_data,
- $signature_data);
+ $signature_data
+ );
$response = SignResponse::fromJson($json);
- $this->assertSame($key_handle, $response->getKeyHandleWeb(),
- 'Key Handle was parsed incorrectly');
- $this->assertSame($counter, $response->getCounter(),
- 'Counter was parsed incorrectly');
- $this->assertSame(1, $response->getUserPresenceByte(),
- 'User presence byte was parsed incorrectly');
- $this->assertSame($sig, $response->getSignature(),
- 'Signature was parsed incorrectly');
+ $this->assertSame(
+ $key_handle,
+ $response->getKeyHandleWeb(),
+ 'Key Handle was parsed incorrectly'
+ );
+ $this->assertSame(
+ $counter,
+ $response->getCounter(),
+ 'Counter was parsed incorrectly'
+ );
+ $this->assertSame(
+ 1,
+ $response->getUserPresenceByte(),
+ 'User presence byte was parsed incorrectly'
+ );
+ $this->assertSame(
+ $sig,
+ $response->getSignature(),
+ 'Signature was parsed incorrectly'
+ );
}
- public function testSignatureWithNullRemainsIntact() {
+ public function testSignatureWithNullRemainsIntact(): void
+ {
$sig = "\x00\x00\x00".random_bytes(10)."\x00\x00\x00";
$sigData = toBase64Web("\x01\x00\x00\x00\x45".$sig);
- $json = sprintf(self::JSON_FORMAT,
+ $json = sprintf(
+ self::JSON_FORMAT,
$this->valid_key_handle,
$this->valid_client_data,
- $sigData);
+ $sigData
+ );
$response = SignResponse::fromJson($json);
- $this->assertSame($sig, $response->getSignature(),
- 'Signature trimmed a trailing NUL byte');
+ $this->assertSame(
+ $sig,
+ $response->getSignature(),
+ 'Signature trimmed a trailing NUL byte'
+ );
}
- public function testSignatureWithSpaceRemainsIntact() {
+ public function testSignatureWithSpaceRemainsIntact(): void
+ {
$sig = ' '.random_bytes(10).' ';
$sigData = toBase64Web("\x01\x00\x00\x00\x45".$sig);
- $json = sprintf(self::JSON_FORMAT,
+ $json = sprintf(
+ self::JSON_FORMAT,
$this->valid_key_handle,
$this->valid_client_data,
- $sigData);
+ $sigData
+ );
$response = SignResponse::fromJson($json);
- $this->assertSame($sig, $response->getSignature(),
- 'Signature trimmed a trailing space');
+ $this->assertSame(
+ $sig,
+ $response->getSignature(),
+ 'Signature trimmed a trailing space'
+ );
}
- public function testFromJsonWithMissingKeyHandle() {
- $json = sprintf('{"clientData":"%s","signatureData":"%s"}',
- $this->valid_client_data, $this->valid_signature_data);
+ public function testFromJsonWithMissingKeyHandle(): void
+ {
+ $json = sprintf(
+ '{"clientData":"%s","signatureData":"%s"}',
+ $this->valid_client_data,
+ $this->valid_signature_data
+ );
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MISSING_KEY);
SignResponse::fromJson($json);
}
- public function testFromJsonWithMissingClientData() {
- $json = sprintf('{"keyHandle":"%s","signatureData":"%s"}',
- $this->valid_key_handle, $this->valid_signature_data);
+ public function testFromJsonWithMissingClientData(): void
+ {
+ $json = sprintf(
+ '{"keyHandle":"%s","signatureData":"%s"}',
+ $this->valid_key_handle,
+ $this->valid_signature_data
+ );
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MISSING_KEY);
SignResponse::fromJson($json);
}
- public function testFromJsonWithMissingSignatureData() {
- $json = sprintf('{"keyHandle":"%s","clientData":"%s"}',
- $this->valid_key_handle, $this->valid_client_data);
+ public function testFromJsonWithMissingSignatureData(): void
+ {
+ $json = sprintf(
+ '{"keyHandle":"%s","clientData":"%s"}',
+ $this->valid_key_handle,
+ $this->valid_client_data
+ );
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MISSING_KEY);
SignResponse::fromJson($json);
}
- public function testFromJsonWithInvalidSignatureData() {
- $json = sprintf('{"keyHandle":"%s","clientData":"%s","signatureData":"%s"}',
+ public function testFromJsonWithInvalidSignatureData(): void
+ {
+ $json = sprintf(
+ '{"keyHandle":"%s","clientData":"%s","signatureData":"%s"}',
$this->valid_key_handle,
$this->valid_client_data,
- '0000000');
+ '0000000'
+ );
$this->expectException(InvalidDataException::class);
$this->expectExceptionCode(InvalidDataException::MALFORMED_DATA);
SignResponse::fromJson($json);
}
+ /**
+ * @covers ::getSignedData
+ */
+ public function testGetSignedData(): void
+ {
+ $json = file_get_contents(__DIR__ . '/sign_response.json');
+ assert($json !== false);
+ $response = SignResponse::fromJson($json);
+
+ $expectedSignedData = sprintf(
+ '%s%s%s%s',
+ hash('sha256', 'https://u2f.ericstern.com', true),
+ "\x01", // user presence
+ "\x00\x00\x00\x2d", // counter (int(45))
+ hash(
+ 'sha256',
+ '{'.
+ '"typ":"navigator.id.getAssertion",'.
+ '"challenge":"wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg",'.
+ '"origin":"https://u2f.ericstern.com",'.
+ '"cid_pubkey":""'.
+ '}',
+ true
+ )
+ );
+
+ $this->assertSame(
+ $expectedSignedData,
+ $response->getSignedData(),
+ 'Wrong signed data'
+ );
+ }
+
+ /**
+ * @covers ::getChallenge
+ */
+ public function testGetChallenge(): void
+ {
+ $json = file_get_contents(__DIR__ . '/sign_response.json');
+ assert($json !== false);
+ $response = SignResponse::fromJson($json);
+
+ $this->assertSame(
+ 'wt2ze8IskcTO3nIsO2D2hFjE5tVD041NpnYesLpJweg',
+ $response->getChallenge()
+ );
+ }
+
/**
* @dataProvider clientErrors
*/
- public function testErrorResponse(int $code) {
+ public function testErrorResponse(int $code): void
+ {
$json = sprintf('{"errorCode":%d}', $code);
$this->expectException(ClientErrorException::class);
$this->expectExceptionCode($code);
SignResponse::fromJson($json);
}
- public function clientErrors() {
+ /** @return array{int}[] */
+ public function clientErrors()
+ {
return [
[ClientError::OTHER_ERROR],
[ClientError::BAD_REQUEST],
@@ -142,5 +253,4 @@ public function clientErrors() {
[ClientError::TIMEOUT],
];
}
-
}
diff --git a/tests/VersionTraitTest.php b/tests/VersionTraitTest.php
index 154f415..03ba974 100644
--- a/tests/VersionTraitTest.php
+++ b/tests/VersionTraitTest.php
@@ -8,19 +8,21 @@
* @covers ::
* @covers ::
*/
-class VersionTraitTest extends \PHPUnit_Framework_TestCase
+class VersionTraitTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers ::getVersion
*/
- public function testGetVersion() {
+ public function testGetVersion(): void
+ {
$obj = new class {
use VersionTrait;
};
- $this->assertSame('U2F_V2', $obj->getVersion(),
- 'getVersion should always return the string "U2F_V2" per the spec');
+ $this->assertSame(
+ 'U2F_V2',
+ $obj->getVersion(),
+ 'getVersion should always return the string "U2F_V2" per the spec'
+ );
}
-
-
}