Update website

This commit is contained in:
Guilhem Lavaux 2024-11-19 08:02:04 +01:00
parent 4413528994
commit 1d90fbf296
6865 changed files with 1091082 additions and 0 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,31 @@
{
"name": "web-auth/cose-lib",
"type": "library",
"license": "MIT",
"description": "CBOR Object Signing and Encryption (COSE) For PHP",
"keywords": ["COSE", "RFC8152"],
"homepage": "https://github.com/web-auth",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/web-auth/cose/contributors"
}
],
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-openssl": "*",
"ext-mbstring": "*",
"fgrosse/phpasn1": "^2.1",
"beberlei/assert": "^3.2"
},
"autoload": {
"psr-4": {
"Cose\\": "src/"
}
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm;
interface Algorithm
{
public static function identifier(): int;
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Mac;
final class HS256 extends Hmac
{
public const ID = 5;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getSignatureLength(): int
{
return 256;
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Mac;
final class HS256Truncated64 extends Hmac
{
public const ID = 4;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getSignatureLength(): int
{
return 64;
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Mac;
final class HS384 extends Hmac
{
public const ID = 6;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): string
{
return 'sha384';
}
protected function getSignatureLength(): int
{
return 384;
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Mac;
final class HS512 extends Hmac
{
public const ID = 7;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): string
{
return 'sha512';
}
protected function getSignatureLength(): int
{
return 512;
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Mac;
use Assert\Assertion;
use Cose\Key\Key;
abstract class Hmac implements Mac
{
public function hash(string $data, Key $key): string
{
$this->checKey($key);
$signature = hash_hmac($this->getHashAlgorithm(), $data, $key->get(-1), true);
return mb_substr($signature, 0, intdiv($this->getSignatureLength(), 8), '8bit');
}
public function verify(string $data, Key $key, string $signature): bool
{
return hash_equals($this->hash($data, $key), $signature);
}
abstract protected function getHashAlgorithm(): string;
abstract protected function getSignatureLength(): int;
private function checKey(Key $key): void
{
Assertion::eq($key->type(), 4, 'Invalid key. Must be of type symmetric');
Assertion::true($key->has(-1), 'Invalid key. The value of the key is missing');
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Mac;
use Cose\Algorithm\Algorithm;
use Cose\Key\Key;
interface Mac extends Algorithm
{
public function hash(string $data, Key $key): string;
public function verify(string $data, Key $key, string $signature): bool;
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm;
use function array_key_exists;
use Assert\Assertion;
class Manager
{
/**
* @var Algorithm[]
*/
private $algorithms = [];
public function add(Algorithm $algorithm): void
{
$identifier = $algorithm::identifier();
$this->algorithms[$identifier] = $algorithm;
}
public function list(): iterable
{
yield from array_keys($this->algorithms);
}
/**
* @return Algorithm[]
*/
public function all(): iterable
{
yield from $this->algorithms;
}
public function has(int $identifier): bool
{
return array_key_exists($identifier, $this->algorithms);
}
public function get(int $identifier): Algorithm
{
Assertion::true($this->has($identifier), 'Unsupported algorithm');
return $this->algorithms[$identifier];
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm;
use Assert\Assertion;
class ManagerFactory
{
/**
* @var Algorithm[]
*/
private $algorithms = [];
public function add(string $alias, Algorithm $algorithm): void
{
$this->algorithms[$alias] = $algorithm;
}
public function list(): iterable
{
yield from array_keys($this->algorithms);
}
public function all(): iterable
{
yield from array_keys($this->algorithms);
}
public function create(array $aliases): Manager
{
$manager = new Manager();
foreach ($aliases as $alias) {
Assertion::keyExists($this->algorithms, $alias, sprintf('The algorithm with alias "%s" is not supported', $alias));
$manager->add($this->algorithms[$alias]);
}
return $manager;
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\ECDSA;
use Assert\Assertion;
use Cose\Algorithm\Signature\Signature;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
abstract class ECDSA implements Signature
{
public function sign(string $data, Key $key): string
{
$key = $this->handleKey($key);
openssl_sign($data, $signature, $key->asPEM(), $this->getHashAlgorithm());
return ECSignature::fromAsn1($signature, $this->getSignaturePartLength());
}
public function verify(string $data, Key $key, string $signature): bool
{
$key = $this->handleKey($key);
$publicKey = $key->toPublic();
$signature = ECSignature::toAsn1($signature, $this->getSignaturePartLength());
return 1 === openssl_verify($data, $signature, $publicKey->asPEM(), $this->getHashAlgorithm());
}
abstract protected function getCurve(): int;
abstract protected function getHashAlgorithm(): int;
abstract protected function getSignaturePartLength(): int;
private function handleKey(Key $key): Ec2Key
{
$key = new Ec2Key($key->getData());
Assertion::eq($key->curve(), $this->getCurve(), 'This key cannot be used with this algorithm');
return $key;
}
}

View file

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\ECDSA;
use function bin2hex;
use function dechex;
use function hexdec;
use InvalidArgumentException;
use function mb_strlen;
use function mb_substr;
use function str_pad;
use const STR_PAD_LEFT;
/**
* @internal
*/
final class ECSignature
{
private const ASN1_SEQUENCE = '30';
private const ASN1_INTEGER = '02';
private const ASN1_MAX_SINGLE_BYTE = 128;
private const ASN1_LENGTH_2BYTES = '81';
private const ASN1_BIG_INTEGER_LIMIT = '7f';
private const ASN1_NEGATIVE_INTEGER = '00';
private const BYTE_SIZE = 2;
public static function toAsn1(string $signature, int $length): string
{
$signature = bin2hex($signature);
if (self::octetLength($signature) !== $length) {
throw new InvalidArgumentException('Invalid signature length.');
}
$pointR = self::preparePositiveInteger(mb_substr($signature, 0, $length, '8bit'));
$pointS = self::preparePositiveInteger(mb_substr($signature, $length, null, '8bit'));
$lengthR = self::octetLength($pointR);
$lengthS = self::octetLength($pointS);
$totalLength = $lengthR + $lengthS + self::BYTE_SIZE + self::BYTE_SIZE;
$lengthPrefix = $totalLength > self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : '';
$bin = hex2bin(
self::ASN1_SEQUENCE
.$lengthPrefix.dechex($totalLength)
.self::ASN1_INTEGER.dechex($lengthR).$pointR
.self::ASN1_INTEGER.dechex($lengthS).$pointS
);
if (false === $bin) {
throw new InvalidArgumentException('Unable to convert into ASN.1');
}
return $bin;
}
public static function fromAsn1(string $signature, int $length): string
{
$message = bin2hex($signature);
$position = 0;
if (self::ASN1_SEQUENCE !== self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
throw new InvalidArgumentException('Invalid data. Should start with a sequence.');
}
// @phpstan-ignore-next-line
if (self::ASN1_LENGTH_2BYTES === self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
$position += self::BYTE_SIZE;
}
$pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
$pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
$bin = hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT).str_pad($pointS, $length, '0', STR_PAD_LEFT));
if (false === $bin) {
throw new InvalidArgumentException('Unable to convert from ASN.1');
}
return $bin;
}
private static function octetLength(string $data): int
{
return intdiv(mb_strlen($data, '8bit'), self::BYTE_SIZE);
}
private static function preparePositiveInteger(string $data): string
{
if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
return self::ASN1_NEGATIVE_INTEGER.$data;
}
while (
self::ASN1_NEGATIVE_INTEGER === mb_substr($data, 0, self::BYTE_SIZE, '8bit')
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT
) {
$data = mb_substr($data, 2, null, '8bit');
}
return $data;
}
private static function readAsn1Content(string $message, int &$position, int $length): string
{
$content = mb_substr($message, $position, $length, '8bit');
$position += $length;
return $content;
}
private static function readAsn1Integer(string $message, int &$position): string
{
if (self::ASN1_INTEGER !== self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
throw new InvalidArgumentException('Invalid data. Should contain an integer.');
}
$length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE));
return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE);
}
private static function retrievePositiveInteger(string $data): string
{
while (
self::ASN1_NEGATIVE_INTEGER === mb_substr($data, 0, self::BYTE_SIZE, '8bit')
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT
) {
$data = mb_substr($data, 2, null, '8bit');
}
return $data;
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\ECDSA;
use Cose\Key\Ec2Key;
final class ES256 extends ECDSA
{
public const ID = -7;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA256;
}
protected function getCurve(): int
{
return Ec2Key::CURVE_P256;
}
protected function getSignaturePartLength(): int
{
return 64;
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\ECDSA;
use Cose\Key\Ec2Key;
final class ES256K extends ECDSA
{
public const ID = -46;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA256;
}
protected function getCurve(): int
{
return Ec2Key::CURVE_P256K;
}
protected function getSignaturePartLength(): int
{
return 64;
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\ECDSA;
use Cose\Key\Ec2Key;
final class ES384 extends ECDSA
{
public const ID = -35;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA384;
}
protected function getCurve(): int
{
return Ec2Key::CURVE_P384;
}
protected function getSignaturePartLength(): int
{
return 96;
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\ECDSA;
use Cose\Key\Ec2Key;
final class ES512 extends ECDSA
{
public const ID = -36;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA512;
}
protected function getCurve(): int
{
return Ec2Key::CURVE_P521;
}
protected function getSignaturePartLength(): int
{
return 132;
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\EdDSA;
use Cose\Key\Key;
final class ED256 extends EdDSA
{
public const ID = -260;
public static function identifier(): int
{
return self::ID;
}
public function sign(string $data, Key $key): string
{
$hashedData = hash('sha256', $data, true);
return parent::sign($hashedData, $key);
}
public function verify(string $data, Key $key, string $signature): bool
{
$hashedData = hash('sha256', $data, true);
return parent::verify($hashedData, $key, $signature);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\EdDSA;
use Cose\Key\Key;
final class ED512 extends EdDSA
{
public const ID = -261;
public static function identifier(): int
{
return self::ID;
}
public function sign(string $data, Key $key): string
{
$hashedData = hash('sha512', $data, true);
return parent::sign($hashedData, $key);
}
public function verify(string $data, Key $key, string $signature): bool
{
$hashedData = hash('sha512', $data, true);
return parent::verify($hashedData, $key, $signature);
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\EdDSA;
final class Ed25519 extends EdDSA
{
public const ID = -8;
public static function identifier(): int
{
return self::ID;
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\EdDSA;
use Assert\Assertion;
use Cose\Algorithm\Signature\Signature;
use Cose\Algorithms;
use Cose\Key\Key;
use Cose\Key\OkpKey;
use InvalidArgumentException;
use function sodium_crypto_sign_detached;
use function sodium_crypto_sign_verify_detached;
class EdDSA implements Signature
{
public function sign(string $data, Key $key): string
{
$key = $this->handleKey($key);
Assertion::true($key->isPrivate(), 'The key is not private');
$x = $key->x();
$d = $key->d();
$secret = $d.$x;
switch ($key->curve()) {
case OkpKey::CURVE_ED25519:
return sodium_crypto_sign_detached($data, $secret);
default:
throw new InvalidArgumentException('Unsupported curve');
}
}
public function verify(string $data, Key $key, string $signature): bool
{
$key = $this->handleKey($key);
switch ($key->curve()) {
case OkpKey::CURVE_ED25519:
return sodium_crypto_sign_verify_detached($signature, $data, $key->x());
default:
throw new InvalidArgumentException('Unsupported curve');
}
}
public static function identifier(): int
{
return Algorithms::COSE_ALGORITHM_EdDSA;
}
private function handleKey(Key $key): OkpKey
{
return new OkpKey($key->getData());
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
use Cose\Hash;
final class PS256 extends PSSRSA
{
public const ID = -37;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): Hash
{
return Hash::sha256();
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
use Cose\Hash;
final class PS384 extends PSSRSA
{
public const ID = -38;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): Hash
{
return Hash::sha384();
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
use Cose\Hash;
final class PS512 extends PSSRSA
{
public const ID = -39;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): Hash
{
return Hash::sha512();
}
}

View file

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
use function ceil;
use function chr;
use Cose\Algorithm\Signature\Signature;
use Cose\BigInteger;
use Cose\Hash;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use function hash_equals;
use InvalidArgumentException;
use function mb_strlen;
use function mb_substr;
use function ord;
use function random_bytes;
use RuntimeException;
use function str_pad;
use function str_repeat;
/**
* @internal
*/
abstract class PSSRSA implements Signature
{
public function sign(string $data, Key $key): string
{
$key = $this->handleKey($key);
$modulusLength = mb_strlen($key->n(), '8bit');
$em = $this->encodeEMSAPSS($data, 8 * $modulusLength - 1, $this->getHashAlgorithm());
$message = BigInteger::createFromBinaryString($em);
$signature = $this->exponentiate($key, $message);
return $this->convertIntegerToOctetString($signature, $modulusLength);
}
public function verify(string $data, Key $key, string $signature): bool
{
$key = $this->handleKey($key);
$modulusLength = mb_strlen($key->n(), '8bit');
if (mb_strlen($signature, '8bit') !== $modulusLength) {
throw new InvalidArgumentException('Invalid modulus length');
}
$s2 = BigInteger::createFromBinaryString($signature);
$m2 = $this->exponentiate($key, $s2);
$em = $this->convertIntegerToOctetString($m2, $modulusLength);
$modBits = 8 * $modulusLength;
return $this->verifyEMSAPSS($data, $em, $modBits - 1, $this->getHashAlgorithm());
}
/**
* Exponentiate with or without Chinese Remainder Theorem.
* Operation with primes 'p' and 'q' is appox. 2x faster.
*/
public function exponentiate(RsaKey $key, BigInteger $c): BigInteger
{
if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare(BigInteger::createFromBinaryString($key->n())) > 0) {
throw new RuntimeException();
}
if ($key->isPublic() || !$key->hasPrimes() || !$key->hasExponents() || !$key->hasCoefficient()) {
return $c->modPow(BigInteger::createFromBinaryString($key->e()), BigInteger::createFromBinaryString($key->n()));
}
[$p, $q] = $key->primes();
[$dP, $dQ] = $key->exponents();
$qInv = BigInteger::createFromBinaryString($key->QInv());
$m1 = $c->modPow($dP, $p);
$m2 = $c->modPow($dQ, $q);
$h = $qInv->multiply($m1->subtract($m2)->add($p))->mod($p);
return $m2->add($h->multiply($q));
}
abstract protected function getHashAlgorithm(): Hash;
private function handleKey(Key $key): RsaKey
{
return new RsaKey($key->getData());
}
private function convertIntegerToOctetString(BigInteger $x, int $xLen): string
{
$x = $x->toBytes();
if (mb_strlen($x, '8bit') > $xLen) {
throw new RuntimeException('Unable to convert the integer');
}
return str_pad($x, $xLen, chr(0), STR_PAD_LEFT);
}
/**
* MGF1.
*/
private function getMGF1(string $mgfSeed, int $maskLen, Hash $mgfHash): string
{
$t = '';
$count = ceil($maskLen / $mgfHash->getLength());
for ($i = 0; $i < $count; ++$i) {
$c = pack('N', $i);
$t .= $mgfHash->hash($mgfSeed.$c);
}
return mb_substr($t, 0, $maskLen, '8bit');
}
/**
* EMSA-PSS-ENCODE.
*/
private function encodeEMSAPSS(string $message, int $modulusLength, Hash $hash): string
{
$emLen = ($modulusLength + 1) >> 3;
$sLen = $hash->getLength();
$mHash = $hash->hash($message);
if ($emLen <= $hash->getLength() + $sLen + 2) {
throw new RuntimeException();
}
$salt = random_bytes($sLen);
$m2 = "\0\0\0\0\0\0\0\0".$mHash.$salt;
$h = $hash->hash($m2);
$ps = str_repeat(chr(0), $emLen - $sLen - $hash->getLength() - 2);
$db = $ps.chr(1).$salt;
$dbMask = $this->getMGF1($h, $emLen - $hash->getLength() - 1, $hash);
$maskedDB = $db ^ $dbMask;
$maskedDB[0] = ~chr(0xFF << ($modulusLength & 7)) & $maskedDB[0];
return $maskedDB.$h.chr(0xBC);
}
/**
* EMSA-PSS-VERIFY.
*/
private function verifyEMSAPSS(string $m, string $em, int $emBits, Hash $hash): bool
{
$emLen = ($emBits + 1) >> 3;
$sLen = $hash->getLength();
$mHash = $hash->hash($m);
if ($emLen < $hash->getLength() + $sLen + 2) {
throw new InvalidArgumentException();
}
if ($em[mb_strlen($em, '8bit') - 1] !== chr(0xBC)) {
throw new InvalidArgumentException();
}
$maskedDB = mb_substr($em, 0, -$hash->getLength() - 1, '8bit');
$h = mb_substr($em, -$hash->getLength() - 1, $hash->getLength(), '8bit');
$temp = chr(0xFF << ($emBits & 7));
if ((~$maskedDB[0] & $temp) !== $temp) {
throw new InvalidArgumentException();
}
$dbMask = $this->getMGF1($h, $emLen - $hash->getLength() - 1, $hash/*MGF*/);
$db = $maskedDB ^ $dbMask;
$db[0] = ~chr(0xFF << ($emBits & 7)) & $db[0];
$temp = $emLen - $hash->getLength() - $sLen - 2;
if (mb_substr($db, 0, $temp, '8bit') !== str_repeat(chr(0), $temp)) {
throw new InvalidArgumentException();
}
if (1 !== ord($db[$temp])) {
throw new InvalidArgumentException();
}
$salt = mb_substr($db, $temp + 1, null, '8bit'); // should be $sLen long
$m2 = "\0\0\0\0\0\0\0\0".$mHash.$salt;
$h2 = $hash->hash($m2);
return hash_equals($h, $h2);
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
final class RS1 extends RSA
{
public const ID = -65535;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA1;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
final class RS256 extends RSA
{
public const ID = -257;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA256;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
final class RS384 extends RSA
{
public const ID = -258;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA384;
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
final class RS512 extends RSA
{
public const ID = -259;
public static function identifier(): int
{
return self::ID;
}
protected function getHashAlgorithm(): int
{
return OPENSSL_ALGO_SHA512;
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature\RSA;
use Assert\Assertion;
use Cose\Algorithm\Signature\Signature;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use InvalidArgumentException;
abstract class RSA implements Signature
{
public function sign(string $data, Key $key): string
{
$key = $this->handleKey($key);
Assertion::true($key->isPrivate(), 'The key is not private');
if (false === openssl_sign($data, $signature, $key->asPem(), $this->getHashAlgorithm())) {
throw new InvalidArgumentException('Unable to sign the data');
}
return $signature;
}
public function verify(string $data, Key $key, string $signature): bool
{
$key = $this->handleKey($key);
return 1 === openssl_verify($data, $signature, $key->asPem(), $this->getHashAlgorithm());
}
abstract protected function getHashAlgorithm(): int;
private function handleKey(Key $key): RsaKey
{
return new RsaKey($key->getData());
}
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Algorithm\Signature;
use Cose\Algorithm\Algorithm;
use Cose\Key\Key;
interface Signature extends Algorithm
{
public function sign(string $data, Key $key): string;
public function verify(string $data, Key $key, string $signature): bool;
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose;
use Assert\Assertion;
use Assert\AssertionFailedException;
/**
* @see https://www.iana.org/assignments/cose/cose.xhtml#algorithms
*/
abstract class Algorithms
{
public const COSE_ALGORITHM_AES_CCM_64_128_256 = 33;
public const COSE_ALGORITHM_AES_CCM_64_128_128 = 32;
public const COSE_ALGORITHM_AES_CCM_16_128_256 = 31;
public const COSE_ALGORITHM_AES_CCM_16_128_128 = 30;
public const COSE_ALGORITHM_AES_MAC_256_128 = 26;
public const COSE_ALGORITHM_AES_MAC_128_128 = 25;
public const COSE_ALGORITHM_CHACHA20_POLY1305 = 24;
public const COSE_ALGORITHM_AES_MAC_256_64 = 15;
public const COSE_ALGORITHM_AES_MAC_128_64 = 14;
public const COSE_ALGORITHM_AES_CCM_64_64_256 = 13;
public const COSE_ALGORITHM_AES_CCM_64_64_128 = 12;
public const COSE_ALGORITHM_AES_CCM_16_64_256 = 11;
public const COSE_ALGORITHM_AES_CCM_16_64_128 = 10;
public const COSE_ALGORITHM_HS512 = 7;
public const COSE_ALGORITHM_HS384 = 6;
public const COSE_ALGORITHM_HS256 = 5;
public const COSE_ALGORITHM_HS256_64 = 4;
public const COSE_ALGORITHM_A256GCM = 3;
public const COSE_ALGORITHM_A192GCM = 2;
public const COSE_ALGORITHM_A128GCM = 1;
public const COSE_ALGORITHM_A128KW = -3;
public const COSE_ALGORITHM_A192KW = -4;
public const COSE_ALGORITHM_A256KW = -5;
public const COSE_ALGORITHM_DIRECT = -6;
public const COSE_ALGORITHM_ES256 = -7;
public const COSE_ALGORITHM_EdDSA = -8;
public const COSE_ALGORITHM_ED256 = -260;
public const COSE_ALGORITHM_ED512 = -261;
public const COSE_ALGORITHM_DIRECT_HKDF_SHA_256 = -10;
public const COSE_ALGORITHM_DIRECT_HKDF_SHA_512 = -11;
public const COSE_ALGORITHM_DIRECT_HKDF_AES_128 = -12;
public const COSE_ALGORITHM_DIRECT_HKDF_AES_256 = -13;
public const COSE_ALGORITHM_ECDH_ES_HKDF_256 = -25;
public const COSE_ALGORITHM_ECDH_ES_HKDF_512 = -26;
public const COSE_ALGORITHM_ECDH_SS_HKDF_256 = -27;
public const COSE_ALGORITHM_ECDH_SS_HKDF_512 = -28;
public const COSE_ALGORITHM_ECDH_ES_A128KW = -29;
public const COSE_ALGORITHM_ECDH_ES_A192KW = -30;
public const COSE_ALGORITHM_ECDH_ES_A256KW = -31;
public const COSE_ALGORITHM_ECDH_SS_A128KW = -32;
public const COSE_ALGORITHM_ECDH_SS_A192KW = -33;
public const COSE_ALGORITHM_ECDH_SS_A256KW = -34;
public const COSE_ALGORITHM_ES384 = -35;
public const COSE_ALGORITHM_ES512 = -36;
public const COSE_ALGORITHM_PS256 = -37;
public const COSE_ALGORITHM_PS384 = -38;
public const COSE_ALGORITHM_PS512 = -39;
public const COSE_ALGORITHM_RSAES_OAEP = -40;
public const COSE_ALGORITHM_RSAES_OAEP_256 = -41;
public const COSE_ALGORITHM_RSAES_OAEP_512 = -42;
public const COSE_ALGORITHM_ES256K = -46;
public const COSE_ALGORITHM_RS256 = -257;
public const COSE_ALGORITHM_RS384 = -258;
public const COSE_ALGORITHM_RS512 = -259;
public const COSE_ALGORITHM_RS1 = -65535;
public const COSE_ALGORITHM_MAP = [
self::COSE_ALGORITHM_ES256 => OPENSSL_ALGO_SHA256,
self::COSE_ALGORITHM_ES384 => OPENSSL_ALGO_SHA384,
self::COSE_ALGORITHM_ES512 => OPENSSL_ALGO_SHA512,
self::COSE_ALGORITHM_RS256 => OPENSSL_ALGO_SHA256,
self::COSE_ALGORITHM_RS384 => OPENSSL_ALGO_SHA384,
self::COSE_ALGORITHM_RS512 => OPENSSL_ALGO_SHA512,
self::COSE_ALGORITHM_RS1 => OPENSSL_ALGO_SHA1,
];
public const COSE_HASH_MAP = [
self::COSE_ALGORITHM_ES256K => 'sha256',
self::COSE_ALGORITHM_ES256 => 'sha256',
self::COSE_ALGORITHM_ES384 => 'sha384',
self::COSE_ALGORITHM_ES512 => 'sha512',
self::COSE_ALGORITHM_RS256 => 'sha256',
self::COSE_ALGORITHM_RS384 => 'sha384',
self::COSE_ALGORITHM_RS512 => 'sha512',
self::COSE_ALGORITHM_PS256 => 'sha256',
self::COSE_ALGORITHM_PS384 => 'sha384',
self::COSE_ALGORITHM_PS512 => 'sha512',
self::COSE_ALGORITHM_RS1 => 'sha1',
];
/**
* @throws AssertionFailedException
*/
public static function getOpensslAlgorithmFor(int $algorithmIdentifier): int
{
Assertion::keyExists(self::COSE_ALGORITHM_MAP, $algorithmIdentifier, 'The specified algorithm identifier is not supported');
return self::COSE_ALGORITHM_MAP[$algorithmIdentifier];
}
/**
* @throws AssertionFailedException
*/
public static function getHashAlgorithmFor(int $algorithmIdentifier): string
{
Assertion::keyExists(self::COSE_HASH_MAP, $algorithmIdentifier, 'The specified algorithm identifier is not supported');
return self::COSE_HASH_MAP[$algorithmIdentifier];
}
}

View file

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose;
use Brick\Math\BigInteger as BrickBigInteger;
use function chr;
use function hex2bin;
use InvalidArgumentException;
use function unpack;
/**
* @internal
*/
class BigInteger
{
/**
* Holds the BigInteger's value.
*
* @var BrickBigInteger
*/
private $value;
private function __construct(BrickBigInteger $value)
{
$this->value = $value;
}
public static function createFromBinaryString(string $value): self
{
$res = unpack('H*', $value);
if (false === $res) {
throw new InvalidArgumentException('Unable to convert the data from binary');
}
$data = current($res);
return new self(BrickBigInteger::fromBase($data, 16));
}
public static function createFromDecimal(int $value): self
{
return new self(BrickBigInteger::of($value));
}
/**
* Converts a BigInteger to a binary string.
*/
public function toBytes(): string
{
if ($this->value->isEqualTo(BrickBigInteger::zero())) {
return '';
}
$temp = $this->value->toBase(16);
$temp = 0 !== (mb_strlen($temp, '8bit') & 1) ? '0'.$temp : $temp;
$temp = hex2bin($temp);
if (false === $temp) {
throw new InvalidArgumentException('Unable to convert the data into binary');
}
return ltrim($temp, chr(0));
}
/**
* Adds two BigIntegers.
*
* @param BigInteger $y
*
* @return BigInteger
*/
public function add(self $y): self
{
$value = $this->value->plus($y->value);
return new self($value);
}
/**
* Subtracts two BigIntegers.
*
* @param BigInteger $y
*
* @return BigInteger
*/
public function subtract(self $y): self
{
$value = $this->value->minus($y->value);
return new self($value);
}
/**
* Multiplies two BigIntegers.
*
* @param BigInteger $x
*
* @return BigInteger
*/
public function multiply(self $x): self
{
$value = $this->value->multipliedBy($x->value);
return new self($value);
}
/**
* Performs modular exponentiation.
*
* @param BigInteger $e
* @param BigInteger $n
*
* @return BigInteger
*/
public function modPow(self $e, self $n): self
{
$value = $this->value->modPow($e->value, $n->value);
return new self($value);
}
/**
* Performs modular exponentiation.
*
* @param BigInteger $d
*
* @return BigInteger
*/
public function mod(self $d): self
{
$value = $this->value->mod($d->value);
return new self($value);
}
/**
* Compares two numbers.
*
* @param BigInteger $y
*/
public function compare(self $y): int
{
return $this->value->compareTo($y->value);
}
}

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose;
/**
* @internal
*/
class Hash
{
/**
* Hash Parameter.
*
* @var string
*/
private $hash;
/**
* DER encoding T.
*
* @var string
*/
private $t;
/**
* Hash Length.
*
* @var int
*/
private $length;
private function __construct(string $hash, int $length, string $t)
{
$this->hash = $hash;
$this->length = $length;
$this->t = $t;
}
/**
* @return Hash
*/
public static function sha1(): self
{
return new self('sha1', 20, "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14");
}
/**
* @return Hash
*/
public static function sha256(): self
{
return new self('sha256', 32, "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20");
}
/**
* @return Hash
*/
public static function sha384(): self
{
return new self('sha384', 48, "\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30");
}
/**
* @return Hash
*/
public static function sha512(): self
{
return new self('sha512', 64, "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40");
}
public function getLength(): int
{
return $this->length;
}
/**
* Compute the HMAC.
*/
public function hash(string $text): string
{
return hash($this->hash, $text, true);
}
public function name(): string
{
return $this->hash;
}
public function t(): string
{
return $this->t;
}
}

View file

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Key;
use function array_key_exists;
use Assert\Assertion;
use FG\ASN1\ExplicitlyTaggedObject;
use FG\ASN1\Universal\BitString;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
class Ec2Key extends Key
{
public const CURVE_P256 = 1;
public const CURVE_P256K = 8;
public const CURVE_P384 = 2;
public const CURVE_P521 = 3;
public const DATA_CURVE = -1;
public const DATA_X = -2;
public const DATA_Y = -3;
public const DATA_D = -4;
private const SUPPORTED_CURVES = [
self::CURVE_P256,
self::CURVE_P256K,
self::CURVE_P384,
self::CURVE_P521,
];
private const NAMED_CURVE_OID = [
self::CURVE_P256 => '1.2.840.10045.3.1.7', // NIST P-256 / secp256r1
self::CURVE_P256K => '1.3.132.0.10', // NIST P-256K / secp256k1
self::CURVE_P384 => '1.3.132.0.34', // NIST P-384 / secp384r1
self::CURVE_P521 => '1.3.132.0.35', // NIST P-521 / secp521r1
];
private const CURVE_KEY_LENGTH = [
self::CURVE_P256 => 32,
self::CURVE_P256K => 32,
self::CURVE_P384 => 48,
self::CURVE_P521 => 66,
];
public function __construct(array $data)
{
parent::__construct($data);
Assertion::eq($data[self::TYPE], self::TYPE_EC2, 'Invalid EC2 key. The key type does not correspond to an EC2 key');
Assertion::keyExists($data, self::DATA_CURVE, 'Invalid EC2 key. The curve is missing');
Assertion::keyExists($data, self::DATA_X, 'Invalid EC2 key. The x coordinate is missing');
Assertion::keyExists($data, self::DATA_Y, 'Invalid EC2 key. The y coordinate is missing');
Assertion::length($data[self::DATA_X], self::CURVE_KEY_LENGTH[$data[self::DATA_CURVE]], 'Invalid length for x coordinate', null, '8bit');
Assertion::length($data[self::DATA_Y], self::CURVE_KEY_LENGTH[$data[self::DATA_CURVE]], 'Invalid length for y coordinate', null, '8bit');
Assertion::inArray((int) $data[self::DATA_CURVE], self::SUPPORTED_CURVES, 'The curve is not supported');
}
public function toPublic(): self
{
$data = $this->getData();
unset($data[self::DATA_D]);
return new self($data);
}
public function x(): string
{
return $this->get(self::DATA_X);
}
public function y(): string
{
return $this->get(self::DATA_Y);
}
public function isPrivate(): bool
{
return array_key_exists(self::DATA_D, $this->getData());
}
public function d(): string
{
Assertion::true($this->isPrivate(), 'The key is not private');
return $this->get(self::DATA_D);
}
public function curve(): int
{
return (int) $this->get(self::DATA_CURVE);
}
public function asPEM(): string
{
if ($this->isPrivate()) {
$der = new Sequence(
new Integer(1),
new OctetString(bin2hex($this->d())),
new ExplicitlyTaggedObject(0, new ObjectIdentifier($this->getCurveOid())),
new ExplicitlyTaggedObject(1, new BitString(bin2hex($this->getUncompressedCoordinates())))
);
return $this->pem('EC PRIVATE KEY', $der->getBinary());
}
$der = new Sequence(
new Sequence(
new ObjectIdentifier('1.2.840.10045.2.1'),
new ObjectIdentifier($this->getCurveOid())
),
new BitString(bin2hex($this->getUncompressedCoordinates()))
);
return $this->pem('PUBLIC KEY', $der->getBinary());
}
public function getUncompressedCoordinates(): string
{
return "\x04".$this->x().$this->y();
}
private function getCurveOid(): string
{
return self::NAMED_CURVE_OID[$this->curve()];
}
private function pem(string $type, string $der): string
{
return sprintf("-----BEGIN %s-----\n", mb_strtoupper($type)).
chunk_split(base64_encode($der), 64, "\n").
sprintf("-----END %s-----\n", mb_strtoupper($type));
}
}

View file

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Key;
use function array_key_exists;
use Assert\Assertion;
class Key
{
public const TYPE = 1;
public const TYPE_OKP = 1;
public const TYPE_EC2 = 2;
public const TYPE_RSA = 3;
public const TYPE_OCT = 4;
public const KID = 2;
public const ALG = 3;
public const KEY_OPS = 4;
public const BASE_IV = 5;
/**
* @var array
*/
private $data;
public function __construct(array $data)
{
Assertion::keyExists($data, self::TYPE, 'Invalid key: the type is not defined');
$this->data = $data;
}
public static function createFromData(array $data): self
{
Assertion::keyExists($data, self::TYPE, 'Invalid key: the type is not defined');
switch ($data[self::TYPE]) {
case 1:
return new OkpKey($data);
case 2:
return new Ec2Key($data);
case 3:
return new RsaKey($data);
case 4:
return new SymmetricKey($data);
default:
return new self($data);
}
}
/**
* @return int|string
*/
public function type()
{
return $this->data[self::TYPE];
}
public function alg(): int
{
return (int) $this->get(self::ALG);
}
public function getData(): array
{
return $this->data;
}
public function has(int $key): bool
{
return array_key_exists($key, $this->data);
}
/**
* @return mixed
*/
public function get(int $key)
{
Assertion::keyExists($this->data, $key, sprintf('The key has no data at index %d', $key));
return $this->data[$key];
}
}

View file

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Key;
use function array_key_exists;
use Assert\Assertion;
class OkpKey extends Key
{
public const CURVE_X25519 = 4;
public const CURVE_X448 = 5;
public const CURVE_ED25519 = 6;
public const CURVE_ED448 = 7;
public const DATA_CURVE = -1;
public const DATA_X = -2;
public const DATA_D = -4;
private const SUPPORTED_CURVES = [
self::CURVE_X25519,
self::CURVE_X448,
self::CURVE_ED25519,
self::CURVE_ED448,
];
public function __construct(array $data)
{
parent::__construct($data);
Assertion::eq($data[self::TYPE], self::TYPE_OKP, 'Invalid OKP key. The key type does not correspond to an OKP key');
Assertion::keyExists($data, self::DATA_CURVE, 'Invalid EC2 key. The curve is missing');
Assertion::keyExists($data, self::DATA_X, 'Invalid OKP key. The x coordinate is missing');
Assertion::inArray((int) $data[self::DATA_CURVE], self::SUPPORTED_CURVES, 'The curve is not supported');
}
public function x(): string
{
return $this->get(self::DATA_X);
}
public function isPrivate(): bool
{
return array_key_exists(self::DATA_D, $this->getData());
}
public function d(): string
{
Assertion::true($this->isPrivate(), 'The key is not private');
return $this->get(self::DATA_D);
}
public function curve(): int
{
return (int) $this->get(self::DATA_CURVE);
}
}

View file

@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Key;
use function array_key_exists;
use Assert\Assertion;
use Brick\Math\BigInteger;
use FG\ASN1\Universal\BitString;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\NullObject;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\Sequence;
use InvalidArgumentException;
class RsaKey extends Key
{
public const DATA_N = -1;
public const DATA_E = -2;
public const DATA_D = -3;
public const DATA_P = -4;
public const DATA_Q = -5;
public const DATA_DP = -6;
public const DATA_DQ = -7;
public const DATA_QI = -8;
public const DATA_OTHER = -9;
public const DATA_RI = -10;
public const DATA_DI = -11;
public const DATA_TI = -12;
public function __construct(array $data)
{
parent::__construct($data);
Assertion::eq($data[self::TYPE], self::TYPE_RSA, 'Invalid RSA key. The key type does not correspond to a RSA key');
Assertion::keyExists($data, self::DATA_N, 'Invalid RSA key. The modulus is missing');
Assertion::keyExists($data, self::DATA_E, 'Invalid RSA key. The exponent is missing');
}
public function n(): string
{
return $this->get(self::DATA_N);
}
public function e(): string
{
return $this->get(self::DATA_E);
}
public function d(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_D);
}
public function p(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_P);
}
public function q(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_Q);
}
public function dP(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_DP);
}
public function dQ(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_DQ);
}
public function QInv(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_QI);
}
public function other(): array
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_OTHER);
}
public function rI(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_RI);
}
public function dI(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_DI);
}
public function tI(): string
{
Assertion::true($this->isPrivate(), 'The key is not private.');
return $this->get(self::DATA_TI);
}
public function hasPrimes(): bool
{
return $this->has(self::DATA_P) && $this->has(self::DATA_Q);
}
public function primes(): array
{
return [
$this->p(),
$this->q(),
];
}
public function hasExponents(): bool
{
return $this->has(self::DATA_DP) && $this->has(self::DATA_DQ);
}
public function exponents(): array
{
return [
$this->dP(),
$this->dQ(),
];
}
public function hasCoefficient(): bool
{
return $this->has(self::DATA_QI);
}
public function isPublic(): bool
{
return !$this->isPrivate();
}
public function isPrivate(): bool
{
return array_key_exists(self::DATA_D, $this->getData());
}
public function asPem(): string
{
Assertion::false($this->isPrivate(), 'Unsupported for private keys.');
$bitSring = new Sequence(
new Integer($this->fromBase64ToInteger($this->n())),
new Integer($this->fromBase64ToInteger($this->e()))
);
$der = new Sequence(
new Sequence(
new ObjectIdentifier('1.2.840.113549.1.1.1'),
new NullObject()
),
new BitString(bin2hex($bitSring->getBinary()))
);
return $this->pem('PUBLIC KEY', $der->getBinary());
}
private function fromBase64ToInteger(string $value): string
{
$data = unpack('H*', $value);
if (false === $data) {
throw new InvalidArgumentException('Unable to convert to an integer');
}
$hex = current($data);
return BigInteger::fromBase($hex, 16)->toBase(10);
}
private function pem(string $type, string $der): string
{
return sprintf("-----BEGIN %s-----\n", mb_strtoupper($type)).
chunk_split(base64_encode($der), 64, "\n").
sprintf("-----END %s-----\n", mb_strtoupper($type));
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose\Key;
use Assert\Assertion;
class SymmetricKey extends Key
{
public const DATA_K = -1;
public function __construct(array $data)
{
parent::__construct($data);
Assertion::eq($data[self::TYPE], self::TYPE_OCT, 'Invalid symmetric key. The key type does not correspond to a symmetric key');
Assertion::keyExists($data, self::DATA_K, 'Invalid symmetric key. The parameter "k" is missing');
}
public function k(): string
{
return $this->get(self::DATA_K);
}
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Cose;
class Verifier
{
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,39 @@
{
"name": "web-auth/metadata-service",
"type": "library",
"license": "MIT",
"description": "Metadata Service for FIDO2/Webauthn",
"keywords": ["FIDO", "FIDO2", "webauthn"],
"homepage": "https://github.com/web-auth",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/web-auth/metadata-service/contributors"
}
],
"require": {
"php": ">=7.2",
"ext-json": "*",
"beberlei/assert": "^3.2",
"league/uri": "^6.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/log": "^1.1"
},
"suggest": {
"psr/log-implementation": "Recommended to receive logs from the library"
},
"autoload": {
"psr-4": {
"Webauthn\\MetadataService\\": "src/"
}
},
"suggest": {
"web-token/jwt-key-mgmt": "Mandatory for fetching Metadata Statement from distant sources",
"web-token/jwt-signature-algorithm-ecdsa": "Mandatory for fetching Metadata Statement from distant sources"
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use JsonSerializable;
abstract class AbstractDescriptor implements JsonSerializable
{
/**
* @var int|null
*/
private $maxRetries;
/**
* @var int|null
*/
private $blockSlowdown;
public function __construct(?int $maxRetries = null, ?int $blockSlowdown = null)
{
Assertion::greaterOrEqualThan($maxRetries, 0, Utils::logicException('Invalid data. The value of "maxRetries" must be a positive integer'));
Assertion::greaterOrEqualThan($blockSlowdown, 0, Utils::logicException('Invalid data. The value of "blockSlowdown" must be a positive integer'));
$this->maxRetries = $maxRetries;
$this->blockSlowdown = $blockSlowdown;
}
public function getMaxRetries(): ?int
{
return $this->maxRetries;
}
public function getBlockSlowdown(): ?int
{
return $this->blockSlowdown;
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
abstract class AuthenticatorStatus
{
public const NOT_FIDO_CERTIFIED = 'NOT_FIDO_CERTIFIED';
public const FIDO_CERTIFIED = 'FIDO_CERTIFIED';
public const USER_VERIFICATION_BYPASS = 'USER_VERIFICATION_BYPASS';
public const ATTESTATION_KEY_COMPROMISE = 'ATTESTATION_KEY_COMPROMISE';
public const USER_KEY_REMOTE_COMPROMISE = 'USER_KEY_REMOTE_COMPROMISE';
public const USER_KEY_PHYSICAL_COMPROMISE = 'USER_KEY_PHYSICAL_COMPROMISE';
public const UPDATE_AVAILABLE = 'UPDATE_AVAILABLE';
public const REVOKED = 'REVOKED';
public const SELF_ASSERTION_SUBMITTED = 'SELF_ASSERTION_SUBMITTED';
public const FIDO_CERTIFIED_L1 = 'FIDO_CERTIFIED_L1';
public const FIDO_CERTIFIED_L1plus = 'FIDO_CERTIFIED_L1plus';
public const FIDO_CERTIFIED_L2 = 'FIDO_CERTIFIED_L2';
public const FIDO_CERTIFIED_L2plus = 'FIDO_CERTIFIED_L2plus';
public const FIDO_CERTIFIED_L3 = 'FIDO_CERTIFIED_L3';
public const FIDO_CERTIFIED_L3plus = 'FIDO_CERTIFIED_L3plus';
public const FIDO_CERTIFIED_L4 = 'FIDO_CERTIFIED_L4';
public const FIDO_CERTIFIED_L5 = 'FIDO_CERTIFIED_L5';
public static function list(): array
{
return [
self::NOT_FIDO_CERTIFIED,
self::FIDO_CERTIFIED,
self::USER_VERIFICATION_BYPASS,
self::ATTESTATION_KEY_COMPROMISE,
self::USER_KEY_REMOTE_COMPROMISE,
self::USER_KEY_PHYSICAL_COMPROMISE,
self::UPDATE_AVAILABLE,
self::REVOKED,
self::SELF_ASSERTION_SUBMITTED,
self::FIDO_CERTIFIED_L1,
self::FIDO_CERTIFIED_L1plus,
self::FIDO_CERTIFIED_L2,
self::FIDO_CERTIFIED_L2plus,
self::FIDO_CERTIFIED_L3,
self::FIDO_CERTIFIED_L3plus,
self::FIDO_CERTIFIED_L4,
self::FIDO_CERTIFIED_L5,
];
}
}

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
class BiometricAccuracyDescriptor extends AbstractDescriptor
{
/**
* @var float|null
*/
private $FAR;
/**
* @var float|null
*/
private $FRR;
/**
* @var float|null
*/
private $EER;
/**
* @var float|null
*/
private $FAAR;
/**
* @var int|null
*/
private $maxReferenceDataSets;
public function __construct(?float $FAR, ?float $FRR, ?float $EER, ?float $FAAR, ?int $maxReferenceDataSets, ?int $maxRetries = null, ?int $blockSlowdown = null)
{
Assertion::greaterOrEqualThan($maxReferenceDataSets, 0, Utils::logicException('Invalid data. The value of "maxReferenceDataSets" must be a positive integer'));
$this->FRR = $FRR;
$this->FAR = $FAR;
$this->EER = $EER;
$this->FAAR = $FAAR;
$this->maxReferenceDataSets = $maxReferenceDataSets;
parent::__construct($maxRetries, $blockSlowdown);
}
public function getFAR(): ?float
{
return $this->FAR;
}
public function getFRR(): ?float
{
return $this->FRR;
}
public function getEER(): ?float
{
return $this->EER;
}
public function getFAAR(): ?float
{
return $this->FAAR;
}
public function getMaxReferenceDataSets(): ?int
{
return $this->maxReferenceDataSets;
}
public static function createFromArray(array $data): self
{
return new self(
$data['FAR'] ?? null,
$data['FRR'] ?? null,
$data['EER'] ?? null,
$data['FAAR'] ?? null,
$data['maxReferenceDataSets'] ?? null,
$data['maxRetries'] ?? null,
$data['blockSlowdown'] ?? null
);
}
public function jsonSerialize(): array
{
$data = [
'FAR' => $this->FAR,
'FRR' => $this->FRR,
'EER' => $this->EER,
'FAAR' => $this->FAAR,
'maxReferenceDataSets' => $this->maxReferenceDataSets,
'maxRetries' => $this->getMaxRetries(),
'blockSlowdown' => $this->getBlockSlowdown(),
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use JsonSerializable;
class BiometricStatusReport implements JsonSerializable
{
/**
* @var int
*/
private $certLevel;
/**
* @var int
*/
private $modality;
/**
* @var string|null
*/
private $effectiveDate;
/**
* @var string|null
*/
private $certificationDescriptor;
/**
* @var string|null
*/
private $certificateNumber;
/**
* @var string|null
*/
private $certificationPolicyVersion;
/**
* @var string|null
*/
private $certificationRequirementsVersion;
public function getCertLevel(): int
{
return $this->certLevel;
}
public function getModality(): int
{
return $this->modality;
}
public function getEffectiveDate(): ?string
{
return $this->effectiveDate;
}
public function getCertificationDescriptor(): ?string
{
return $this->certificationDescriptor;
}
public function getCertificateNumber(): ?string
{
return $this->certificateNumber;
}
public function getCertificationPolicyVersion(): ?string
{
return $this->certificationPolicyVersion;
}
public function getCertificationRequirementsVersion(): ?string
{
return $this->certificationRequirementsVersion;
}
public static function createFromArray(array $data): self
{
$object = new self();
$object->certLevel = $data['certLevel'] ?? null;
$object->modality = $data['modality'] ?? null;
$object->effectiveDate = $data['effectiveDate'] ?? null;
$object->certificationDescriptor = $data['certificationDescriptor'] ?? null;
$object->certificateNumber = $data['certificateNumber'] ?? null;
$object->certificationPolicyVersion = $data['certificationPolicyVersion'] ?? null;
$object->certificationRequirementsVersion = $data['certificationRequirementsVersion'] ?? null;
return $object;
}
public function jsonSerialize(): array
{
$data = [
'certLevel' => $this->certLevel,
'modality' => $this->modality,
'effectiveDate' => $this->effectiveDate,
'certificationDescriptor' => $this->certificationDescriptor,
'certificateNumber' => $this->certificateNumber,
'certificationPolicyVersion' => $this->certificationPolicyVersion,
'certificationRequirementsVersion' => $this->certificationRequirementsVersion,
];
return array_filter($data, static function ($var): bool {return null !== $var; });
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
class CodeAccuracyDescriptor extends AbstractDescriptor
{
/**
* @var int
*/
private $base;
/**
* @var int
*/
private $minLength;
public function __construct(int $base, int $minLength, ?int $maxRetries = null, ?int $blockSlowdown = null)
{
Assertion::greaterOrEqualThan($base, 0, Utils::logicException('Invalid data. The value of "base" must be a positive integer'));
Assertion::greaterOrEqualThan($minLength, 0, Utils::logicException('Invalid data. The value of "minLength" must be a positive integer'));
$this->base = $base;
$this->minLength = $minLength;
parent::__construct($maxRetries, $blockSlowdown);
}
public function getBase(): int
{
return $this->base;
}
public function getMinLength(): int
{
return $this->minLength;
}
public static function createFromArray(array $data): self
{
Assertion::keyExists($data, 'base', Utils::logicException('The parameter "base" is missing'));
Assertion::keyExists($data, 'minLength', Utils::logicException('The parameter "minLength" is missing'));
return new self(
$data['base'],
$data['minLength'],
$data['maxRetries'] ?? null,
$data['blockSlowdown'] ?? null
);
}
public function jsonSerialize(): array
{
$data = [
'base' => $this->base,
'minLength' => $this->minLength,
'maxRetries' => $this->getMaxRetries(),
'blockSlowdown' => $this->getBlockSlowdown(),
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use JsonSerializable;
use function Safe\sprintf;
class DisplayPNGCharacteristicsDescriptor implements JsonSerializable
{
/**
* @var int
*/
private $width;
/**
* @var int
*/
private $height;
/**
* @var int
*/
private $bitDepth;
/**
* @var int
*/
private $colorType;
/**
* @var int
*/
private $compression;
/**
* @var int
*/
private $filter;
/**
* @var int
*/
private $interlace;
/**
* @var RgbPaletteEntry[]
*/
private $plte = [];
public function __construct(int $width, int $height, int $bitDepth, int $colorType, int $compression, int $filter, int $interlace)
{
Assertion::greaterOrEqualThan($width, 0, Utils::logicException('Invalid width'));
Assertion::greaterOrEqualThan($height, 0, Utils::logicException('Invalid height'));
Assertion::range($bitDepth, 0, 254, Utils::logicException('Invalid bit depth'));
Assertion::range($colorType, 0, 254, Utils::logicException('Invalid color type'));
Assertion::range($compression, 0, 254, Utils::logicException('Invalid compression'));
Assertion::range($filter, 0, 254, Utils::logicException('Invalid filter'));
Assertion::range($interlace, 0, 254, Utils::logicException('Invalid interlace'));
$this->width = $width;
$this->height = $height;
$this->bitDepth = $bitDepth;
$this->colorType = $colorType;
$this->compression = $compression;
$this->filter = $filter;
$this->interlace = $interlace;
}
public function addPalette(RgbPaletteEntry $rgbPaletteEntry): self
{
$this->plte[] = $rgbPaletteEntry;
return $this;
}
public function getWidth(): int
{
return $this->width;
}
public function getHeight(): int
{
return $this->height;
}
public function getBitDepth(): int
{
return $this->bitDepth;
}
public function getColorType(): int
{
return $this->colorType;
}
public function getCompression(): int
{
return $this->compression;
}
public function getFilter(): int
{
return $this->filter;
}
public function getInterlace(): int
{
return $this->interlace;
}
/**
* @return RgbPaletteEntry[]
*/
public function getPlte(): array
{
return $this->plte;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['width', 'compression', 'height', 'bitDepth', 'colorType', 'compression', 'filter', 'interlace'] as $key) {
Assertion::keyExists($data, $key, sprintf('Invalid data. The key "%s" is missing', $key));
}
$object = new self(
$data['width'],
$data['height'],
$data['bitDepth'],
$data['colorType'],
$data['compression'],
$data['filter'],
$data['interlace']
);
if (isset($data['plte'])) {
$plte = $data['plte'];
Assertion::isArray($plte, Utils::logicException('Invalid "plte" parameter'));
foreach ($plte as $item) {
$object->addPalette(RgbPaletteEntry::createFromArray($item));
}
}
return $object;
}
public function jsonSerialize(): array
{
$data = [
'width' => $this->width,
'height' => $this->height,
'bitDepth' => $this->bitDepth,
'colorType' => $this->colorType,
'compression' => $this->compression,
'filter' => $this->filter,
'interlace' => $this->interlace,
'plte' => $this->plte,
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use Base64Url\Base64Url;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use function Safe\json_decode;
use function Safe\sprintf;
class DistantSingleMetadata extends SingleMetadata
{
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var array
*/
private $additionalHeaders;
/**
* @var string
*/
private $uri;
/**
* @var bool
*/
private $isBase64Encoded;
public function __construct(string $uri, bool $isBase64Encoded, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, array $additionalHeaders = [])
{
parent::__construct($uri, $isBase64Encoded); //Useless
$this->uri = $uri;
$this->isBase64Encoded = $isBase64Encoded;
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
$this->additionalHeaders = $additionalHeaders;
}
public function getMetadataStatement(): MetadataStatement
{
$payload = $this->fetch();
$json = $this->isBase64Encoded ? Base64Url::decode($payload) : $payload;
$data = json_decode($json, true);
return MetadataStatement::createFromArray($data);
}
private function fetch(): string
{
$request = $this->requestFactory->createRequest('GET', $this->uri);
foreach ($this->additionalHeaders as $k => $v) {
$request = $request->withHeader($k, $v);
}
$response = $this->httpClient->sendRequest($request);
Assertion::eq(200, $response->getStatusCode(), sprintf('Unable to contact the server. Response code is %d', $response->getStatusCode()));
$content = $response->getBody()->getContents();
Assertion::notEmpty($content, 'Unable to contact the server. The response has no content');
return $content;
}
}

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use Base64Url\Base64Url;
use JsonSerializable;
use function Safe\sprintf;
class EcdaaTrustAnchor implements JsonSerializable
{
/**
* @var string
*/
private $X;
/**
* @var string
*/
private $Y;
/**
* @var string
*/
private $c;
/**
* @var string
*/
private $sx;
/**
* @var string
*/
private $sy;
/**
* @var string
*/
private $G1Curve;
public function __construct(string $X, string $Y, string $c, string $sx, string $sy, string $G1Curve)
{
$this->X = $X;
$this->Y = $Y;
$this->c = $c;
$this->sx = $sx;
$this->sy = $sy;
$this->G1Curve = $G1Curve;
}
public function getX(): string
{
return $this->X;
}
public function getY(): string
{
return $this->Y;
}
public function getC(): string
{
return $this->c;
}
public function getSx(): string
{
return $this->sx;
}
public function getSy(): string
{
return $this->sy;
}
public function getG1Curve(): string
{
return $this->G1Curve;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['X', 'Y', 'c', 'sx', 'sy', 'G1Curve'] as $key) {
Assertion::keyExists($data, $key, sprintf('Invalid data. The key "%s" is missing', $key));
}
return new self(
Base64Url::decode($data['X']),
Base64Url::decode($data['Y']),
Base64Url::decode($data['c']),
Base64Url::decode($data['sx']),
Base64Url::decode($data['sy']),
$data['G1Curve']
);
}
public function jsonSerialize(): array
{
$data = [
'X' => Base64Url::encode($this->X),
'Y' => Base64Url::encode($this->Y),
'c' => Base64Url::encode($this->c),
'sx' => Base64Url::encode($this->sx),
'sy' => Base64Url::encode($this->sy),
'G1Curve' => $this->G1Curve,
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use function array_key_exists;
use Assert\Assertion;
use JsonSerializable;
class ExtensionDescriptor implements JsonSerializable
{
/**
* @var string
*/
private $id;
/**
* @var int|null
*/
private $tag;
/**
* @var string|null
*/
private $data;
/**
* @var bool
*/
private $fail_if_unknown;
public function __construct(string $id, ?int $tag, ?string $data, bool $fail_if_unknown)
{
if (null !== $tag) {
Assertion::greaterOrEqualThan($tag, 0, Utils::logicException('Invalid data. The parameter "tag" shall be a positive integer'));
}
$this->id = $id;
$this->tag = $tag;
$this->data = $data;
$this->fail_if_unknown = $fail_if_unknown;
}
public function getId(): string
{
return $this->id;
}
public function getTag(): ?int
{
return $this->tag;
}
public function getData(): ?string
{
return $this->data;
}
public function isFailIfUnknown(): bool
{
return $this->fail_if_unknown;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
Assertion::keyExists($data, 'id', Utils::logicException('Invalid data. The parameter "id" is missing'));
Assertion::string($data['id'], Utils::logicException('Invalid data. The parameter "id" shall be a string'));
Assertion::keyExists($data, 'fail_if_unknown', Utils::logicException('Invalid data. The parameter "fail_if_unknown" is missing'));
Assertion::boolean($data['fail_if_unknown'], Utils::logicException('Invalid data. The parameter "fail_if_unknown" shall be a boolean'));
if (array_key_exists('tag', $data)) {
Assertion::integer($data['tag'], Utils::logicException('Invalid data. The parameter "tag" shall be a positive integer'));
}
if (array_key_exists('data', $data)) {
Assertion::string($data['data'], Utils::logicException('Invalid data. The parameter "data" shall be a string'));
}
return new self(
$data['id'],
$data['tag'] ?? null,
$data['data'] ?? null,
$data['fail_if_unknown']
);
}
public function jsonSerialize(): array
{
$result = [
'id' => $this->id,
'tag' => $this->tag,
'data' => $this->data,
'fail_if_unknown' => $this->fail_if_unknown,
];
return Utils::filterNullValues($result);
}
}

View file

@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use Base64Url\Base64Url;
use function count;
use InvalidArgumentException;
use function is_array;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Serializer\CompactSerializer;
use League\Uri\UriString;
use LogicException;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use function Safe\json_decode;
use function Safe\sprintf;
use Throwable;
use Webauthn\CertificateToolbox;
class MetadataService
{
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var array
*/
private $additionalQueryStringValues;
/**
* @var array
*/
private $additionalHeaders;
/**
* @var string
*/
private $serviceUri;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(string $serviceUri, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, array $additionalQueryStringValues = [], array $additionalHeaders = [], ?LoggerInterface $logger = null)
{
if (0 !== count($additionalQueryStringValues)) {
@trigger_error('The argument "additionalQueryStringValues" is deprecated since version 3.3 and will be removed in 4.0. Please set an empty array instead and us the method `addQueryStringValues`.', E_USER_DEPRECATED);
}
if (0 !== count($additionalQueryStringValues)) {
@trigger_error('The argument "additionalHeaders" is deprecated since version 3.3 and will be removed in 4.0. Please set an empty array instead and us the method `addHeaders`.', E_USER_DEPRECATED);
}
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger" instead.', E_USER_DEPRECATED);
}
$this->serviceUri = $serviceUri;
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
$this->additionalQueryStringValues = $additionalQueryStringValues;
$this->additionalHeaders = $additionalHeaders;
$this->logger = $logger ?? new NullLogger();
}
public function addQueryStringValues(array $additionalQueryStringValues): self
{
$this->additionalQueryStringValues = $additionalQueryStringValues;
return $this;
}
public function addHeaders(array $additionalHeaders): self
{
$this->additionalHeaders = $additionalHeaders;
return $this;
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function has(string $aaguid): bool
{
try {
$toc = $this->fetchMetadataTOCPayload();
} catch (Throwable $e) {
return false;
}
foreach ($toc->getEntries() as $entry) {
if ($entry->getAaguid() === $aaguid && null !== $entry->getUrl()) {
return true;
}
}
return false;
}
public function get(string $aaguid): MetadataStatement
{
$toc = $this->fetchMetadataTOCPayload();
foreach ($toc->getEntries() as $entry) {
if ($entry->getAaguid() === $aaguid && null !== $entry->getUrl()) {
$mds = $this->fetchMetadataStatementFor($entry);
$mds
->setStatusReports($entry->getStatusReports())
->setRootCertificates($toc->getRootCertificates())
;
return $mds;
}
}
throw new InvalidArgumentException(sprintf('The Metadata Statement with AAGUID "%s" is missing', $aaguid));
}
/**
* @deprecated This method is deprecated since v3.3 and will be removed in v4.0
*/
public function getMetadataStatementFor(MetadataTOCPayloadEntry $entry, string $hashingFunction = 'sha256'): MetadataStatement
{
return $this->fetchMetadataStatementFor($entry, $hashingFunction);
}
public function fetchMetadataStatementFor(MetadataTOCPayloadEntry $entry, string $hashingFunction = 'sha256'): MetadataStatement
{
$this->logger->info('Trying to get the metadata statement for a given entry', ['entry' => $entry]);
try {
$hash = $entry->getHash();
$url = $entry->getUrl();
if (null === $hash || null === $url) {
throw new LogicException('The Metadata Statement has not been published');
}
$uri = $this->buildUri($url);
$result = $this->fetchMetadataStatement($uri, true, $hash, $hashingFunction);
$this->logger->info('The metadata statement exists');
$this->logger->debug('Metadata Statement', ['mds' => $result]);
return $result;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
/**
* @deprecated This method is deprecated since v3.3 and will be removed in v4.0
*/
public function getMetadataTOCPayload(): MetadataTOCPayload
{
return $this->fetchMetadataTOCPayload();
}
private function fetchMetadataTOCPayload(): MetadataTOCPayload
{
$this->logger->info('Trying to get the metadata service TOC payload');
try {
$uri = $this->buildUri($this->serviceUri);
$toc = $this->fetchTableOfContent($uri);
$this->logger->info('The TOC payload has been received');
$this->logger->debug('TOC payload', ['toc' => $toc]);
return $toc;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
private function buildUri(string $uri): string
{
$parsedUri = UriString::parse($uri);
$queryString = $parsedUri['query'];
$query = [];
if (null !== $queryString) {
parse_str($queryString, $query);
}
foreach ($this->additionalQueryStringValues as $k => $v) {
if (!isset($query[$k])) {
$query[$k] = $v;
continue;
}
if (!is_array($query[$k])) {
$query[$k] = [$query[$k], $v];
continue;
}
$query[$k][] = $v;
}
$parsedUri['query'] = 0 === count($query) ? null : http_build_query($query, '', '&', PHP_QUERY_RFC3986);
return UriString::build($parsedUri);
}
private function fetchTableOfContent(string $uri): MetadataTOCPayload
{
$content = $this->fetch($uri);
$rootCertificates = [];
$payload = $this->getJwsPayload($content, $rootCertificates);
$data = json_decode($payload, true);
$toc = MetadataTOCPayload::createFromArray($data);
$toc->setRootCertificates($rootCertificates);
return $toc;
}
private function fetchMetadataStatement(string $uri, bool $isBase64UrlEncoded, string $hash = '', string $hashingFunction = 'sha256'): MetadataStatement
{
$payload = $this->fetch($uri);
if ('' !== $hash) {
Assertion::true(hash_equals($hash, hash($hashingFunction, $payload, true)), 'The hash cannot be verified. The metadata statement shall be rejected');
}
$json = $isBase64UrlEncoded ? Base64Url::decode($payload) : $payload;
$data = json_decode($json, true);
return MetadataStatement::createFromArray($data);
}
private function fetch(string $uri): string
{
$request = $this->requestFactory->createRequest('GET', $uri);
foreach ($this->additionalHeaders as $k => $v) {
$request = $request->withHeader($k, $v);
}
$response = $this->httpClient->sendRequest($request);
Assertion::eq(200, $response->getStatusCode(), sprintf('Unable to contact the server. Response code is %d', $response->getStatusCode()));
$content = $response->getBody()->getContents();
Assertion::notEmpty($content, 'Unable to contact the server. The response has no content');
return $content;
}
private function getJwsPayload(string $token, array &$rootCertificates): string
{
$jws = (new CompactSerializer())->unserialize($token);
Assertion::eq(1, $jws->countSignatures(), 'Invalid response from the metadata service. Only one signature shall be present.');
$signature = $jws->getSignature(0);
$payload = $jws->getPayload();
Assertion::notEmpty($payload, 'Invalid response from the metadata service. The token payload is empty.');
$header = $signature->getProtectedHeader();
Assertion::keyExists($header, 'alg', 'The "alg" parameter is missing.');
Assertion::eq($header['alg'], 'ES256', 'The expected "alg" parameter value should be "ES256".');
Assertion::keyExists($header, 'x5c', 'The "x5c" parameter is missing.');
Assertion::isArray($header['x5c'], 'The "x5c" parameter should be an array.');
$key = JWKFactory::createFromX5C($header['x5c']);
$rootCertificates = array_map(static function (string $x509): string {
return CertificateToolbox::fixPEMStructure($x509);
}, $header['x5c']);
$algorithm = new ES256();
$isValid = $algorithm->verify($key, $signature->getEncodedProtectedHeader().'.'.$jws->getEncodedPayload(), $signature->getSignature());
Assertion::true($isValid, 'Invalid response from the metadata service. The token signature is invalid.');
return $jws->getPayload();
}
}

View file

@ -0,0 +1,602 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use InvalidArgumentException;
use JsonSerializable;
use function Safe\json_decode;
use function Safe\sprintf;
class MetadataStatement implements JsonSerializable
{
public const KEY_PROTECTION_SOFTWARE = 0x0001;
public const KEY_PROTECTION_HARDWARE = 0x0002;
public const KEY_PROTECTION_TEE = 0x0004;
public const KEY_PROTECTION_SECURE_ELEMENT = 0x0008;
public const KEY_PROTECTION_REMOTE_HANDLE = 0x0010;
public const MATCHER_PROTECTION_SOFTWARE = 0x0001;
public const MATCHER_PROTECTION_TEE = 0x0002;
public const MATCHER_PROTECTION_ON_CHIP = 0x0004;
public const ATTACHMENT_HINT_INTERNAL = 0x0001;
public const ATTACHMENT_HINT_EXTERNAL = 0x0002;
public const ATTACHMENT_HINT_WIRED = 0x0004;
public const ATTACHMENT_HINT_WIRELESS = 0x0008;
public const ATTACHMENT_HINT_NFC = 0x0010;
public const ATTACHMENT_HINT_BLUETOOTH = 0x0020;
public const ATTACHMENT_HINT_NETWORK = 0x0040;
public const ATTACHMENT_HINT_READY = 0x0080;
public const ATTACHMENT_HINT_WIFI_DIRECT = 0x0100;
public const TRANSACTION_CONFIRMATION_DISPLAY_ANY = 0x0001;
public const TRANSACTION_CONFIRMATION_DISPLAY_PRIVILEGED_SOFTWARE = 0x0002;
public const TRANSACTION_CONFIRMATION_DISPLAY_TEE = 0x0004;
public const TRANSACTION_CONFIRMATION_DISPLAY_HARDWARE = 0x0008;
public const TRANSACTION_CONFIRMATION_DISPLAY_REMOTE = 0x0010;
public const ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW = 0x0001;
public const ALG_SIGN_SECP256R1_ECDSA_SHA256_DER = 0x0002;
public const ALG_SIGN_RSASSA_PSS_SHA256_RAW = 0x0003;
public const ALG_SIGN_RSASSA_PSS_SHA256_DER = 0x0004;
public const ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW = 0x0005;
public const ALG_SIGN_SECP256K1_ECDSA_SHA256_DER = 0x0006;
public const ALG_SIGN_SM2_SM3_RAW = 0x0007;
public const ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW = 0x0008;
public const ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER = 0x0009;
public const ALG_SIGN_RSASSA_PSS_SHA384_RAW = 0x000A;
public const ALG_SIGN_RSASSA_PSS_SHA512_RAW = 0x000B;
public const ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW = 0x000C;
public const ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW = 0x000D;
public const ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW = 0x000E;
public const ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW = 0x000F;
public const ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW = 0x0010;
public const ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW = 0x0011;
public const ALG_SIGN_ED25519_EDDSA_SHA256_RAW = 0x0012;
public const ALG_KEY_ECC_X962_RAW = 0x0100;
public const ALG_KEY_ECC_X962_DER = 0x0101;
public const ALG_KEY_RSA_2048_RAW = 0x0102;
public const ALG_KEY_RSA_2048_DER = 0x0103;
public const ALG_KEY_COSE = 0x0104;
public const ATTESTATION_BASIC_FULL = 0x3E07;
public const ATTESTATION_BASIC_SURROGATE = 0x3E08;
public const ATTESTATION_ECDAA = 0x3E09;
public const ATTESTATION_ATTCA = 0x3E0A;
/**
* @var string|null
*/
private $legalHeader;
/**
* @var string|null
*/
private $aaid;
/**
* @var string|null
*/
private $aaguid;
/**
* @var string[]
*/
private $attestationCertificateKeyIdentifiers = [];
/**
* @var string
*/
private $description;
/**
* @var string[]
*/
private $alternativeDescriptions = [];
/**
* @var int
*/
private $authenticatorVersion;
/**
* @var string
*/
private $protocolFamily;
/**
* @var Version[]
*/
private $upv = [];
/**
* @var string|null
*/
private $assertionScheme;
/**
* @var int|null
*/
private $authenticationAlgorithm;
/**
* @var int[]
*/
private $authenticationAlgorithms = [];
/**
* @var int|null
*/
private $publicKeyAlgAndEncoding;
/**
* @var int[]
*/
private $publicKeyAlgAndEncodings = [];
/**
* @var int[]
*/
private $attestationTypes = [];
/**
* @var VerificationMethodANDCombinations[]
*/
private $userVerificationDetails = [];
/**
* @var int
*/
private $keyProtection;
/**
* @var bool|null
*/
private $isKeyRestricted;
/**
* @var bool|null
*/
private $isFreshUserVerificationRequired;
/**
* @var int
*/
private $matcherProtection;
/**
* @var int|null
*/
private $cryptoStrength;
/**
* @var string|null
*/
private $operatingEnv;
/**
* @var int
*/
private $attachmentHint = 0;
/**
* @var bool|null
*/
private $isSecondFactorOnly;
/**
* @var int
*/
private $tcDisplay;
/**
* @var string|null
*/
private $tcDisplayContentType;
/**
* @var DisplayPNGCharacteristicsDescriptor[]
*/
private $tcDisplayPNGCharacteristics = [];
/**
* @var string[]
*/
private $attestationRootCertificates = [];
/**
* @var EcdaaTrustAnchor[]
*/
private $ecdaaTrustAnchors = [];
/**
* @var string|null
*/
private $icon;
/**
* @var ExtensionDescriptor[]
*/
private $supportedExtensions = [];
/**
* @var array<int, StatusReport>
*/
private $statusReports = [];
/**
* @var string[]
*/
private $rootCertificates = [];
public static function createFromString(string $statement): self
{
$data = json_decode($statement, true);
Assertion::isArray($data, 'Invalid Metadata Statement');
return self::createFromArray($data);
}
public function getLegalHeader(): ?string
{
return $this->legalHeader;
}
public function getAaid(): ?string
{
return $this->aaid;
}
public function getAaguid(): ?string
{
return $this->aaguid;
}
/**
* @return string[]
*/
public function getAttestationCertificateKeyIdentifiers(): array
{
return $this->attestationCertificateKeyIdentifiers;
}
public function getDescription(): string
{
return $this->description;
}
/**
* @return string[]
*/
public function getAlternativeDescriptions(): array
{
return $this->alternativeDescriptions;
}
public function getAuthenticatorVersion(): int
{
return $this->authenticatorVersion;
}
public function getProtocolFamily(): string
{
return $this->protocolFamily;
}
/**
* @return Version[]
*/
public function getUpv(): array
{
return $this->upv;
}
public function getAssertionScheme(): ?string
{
return $this->assertionScheme;
}
public function getAuthenticationAlgorithm(): ?int
{
return $this->authenticationAlgorithm;
}
/**
* @return int[]
*/
public function getAuthenticationAlgorithms(): array
{
return $this->authenticationAlgorithms;
}
public function getPublicKeyAlgAndEncoding(): ?int
{
return $this->publicKeyAlgAndEncoding;
}
/**
* @return int[]
*/
public function getPublicKeyAlgAndEncodings(): array
{
return $this->publicKeyAlgAndEncodings;
}
/**
* @return int[]
*/
public function getAttestationTypes(): array
{
return $this->attestationTypes;
}
/**
* @return VerificationMethodANDCombinations[]
*/
public function getUserVerificationDetails(): array
{
return $this->userVerificationDetails;
}
public function getKeyProtection(): int
{
return $this->keyProtection;
}
public function isKeyRestricted(): ?bool
{
return (bool) $this->isKeyRestricted;
}
public function isFreshUserVerificationRequired(): ?bool
{
return (bool) $this->isFreshUserVerificationRequired;
}
public function getMatcherProtection(): int
{
return $this->matcherProtection;
}
public function getCryptoStrength(): ?int
{
return $this->cryptoStrength;
}
public function getOperatingEnv(): ?string
{
return $this->operatingEnv;
}
public function getAttachmentHint(): int
{
return $this->attachmentHint;
}
public function isSecondFactorOnly(): ?bool
{
return (bool) $this->isSecondFactorOnly;
}
public function getTcDisplay(): int
{
return $this->tcDisplay;
}
public function getTcDisplayContentType(): ?string
{
return $this->tcDisplayContentType;
}
/**
* @return DisplayPNGCharacteristicsDescriptor[]
*/
public function getTcDisplayPNGCharacteristics(): array
{
return $this->tcDisplayPNGCharacteristics;
}
/**
* @return string[]
*/
public function getAttestationRootCertificates(): array
{
return $this->attestationRootCertificates;
}
/**
* @return EcdaaTrustAnchor[]
*/
public function getEcdaaTrustAnchors(): array
{
return $this->ecdaaTrustAnchors;
}
public function getIcon(): ?string
{
return $this->icon;
}
/**
* @return ExtensionDescriptor[]
*/
public function getSupportedExtensions(): array
{
return $this->supportedExtensions;
}
public static function createFromArray(array $data): self
{
$object = new self();
foreach (['description', 'protocolFamily'] as $key) {
if (!isset($data[$key])) {
throw new InvalidArgumentException(sprintf('The parameter "%s" is missing', $key));
}
}
$object->legalHeader = $data['legalHeader'] ?? null;
$object->aaid = $data['aaid'] ?? null;
$object->aaguid = $data['aaguid'] ?? null;
$object->attestationCertificateKeyIdentifiers = $data['attestationCertificateKeyIdentifiers'] ?? [];
$object->description = $data['description'];
$object->alternativeDescriptions = $data['alternativeDescriptions'] ?? [];
$object->authenticatorVersion = $data['authenticatorVersion'] ?? 0;
$object->protocolFamily = $data['protocolFamily'];
if (isset($data['upv'])) {
$upv = $data['upv'];
Assertion::isArray($upv, 'Invalid Metadata Statement');
foreach ($upv as $value) {
Assertion::isArray($value, 'Invalid Metadata Statement');
$object->upv[] = Version::createFromArray($value);
}
}
$object->assertionScheme = $data['assertionScheme'] ?? null;
$object->authenticationAlgorithm = $data['authenticationAlgorithm'] ?? null;
$object->authenticationAlgorithms = $data['authenticationAlgorithms'] ?? [];
$object->publicKeyAlgAndEncoding = $data['publicKeyAlgAndEncoding'] ?? null;
$object->publicKeyAlgAndEncodings = $data['publicKeyAlgAndEncodings'] ?? [];
$object->attestationTypes = $data['attestationTypes'] ?? [];
if (isset($data['userVerificationDetails'])) {
$userVerificationDetails = $data['userVerificationDetails'];
Assertion::isArray($userVerificationDetails, 'Invalid Metadata Statement');
foreach ($userVerificationDetails as $value) {
Assertion::isArray($value, 'Invalid Metadata Statement');
$object->userVerificationDetails[] = VerificationMethodANDCombinations::createFromArray($value);
}
}
$object->keyProtection = $data['keyProtection'] ?? 0;
$object->isKeyRestricted = $data['isKeyRestricted'] ?? null;
$object->isFreshUserVerificationRequired = $data['isFreshUserVerificationRequired'] ?? null;
$object->matcherProtection = $data['matcherProtection'] ?? 0;
$object->cryptoStrength = $data['cryptoStrength'] ?? null;
$object->operatingEnv = $data['operatingEnv'] ?? null;
$object->attachmentHint = $data['attachmentHint'] ?? 0;
$object->isSecondFactorOnly = $data['isSecondFactorOnly'] ?? null;
$object->tcDisplay = $data['tcDisplay'] ?? 0;
$object->tcDisplayContentType = $data['tcDisplayContentType'] ?? null;
if (isset($data['tcDisplayPNGCharacteristics'])) {
$tcDisplayPNGCharacteristics = $data['tcDisplayPNGCharacteristics'];
Assertion::isArray($tcDisplayPNGCharacteristics, 'Invalid Metadata Statement');
foreach ($tcDisplayPNGCharacteristics as $tcDisplayPNGCharacteristic) {
Assertion::isArray($tcDisplayPNGCharacteristic, 'Invalid Metadata Statement');
$object->tcDisplayPNGCharacteristics[] = DisplayPNGCharacteristicsDescriptor::createFromArray($tcDisplayPNGCharacteristic);
}
}
$object->attestationRootCertificates = $data['attestationRootCertificates'] ?? [];
$object->ecdaaTrustAnchors = $data['ecdaaTrustAnchors'] ?? [];
$object->icon = $data['icon'] ?? null;
if (isset($data['supportedExtensions'])) {
$supportedExtensions = $data['supportedExtensions'];
Assertion::isArray($supportedExtensions, 'Invalid Metadata Statement');
foreach ($supportedExtensions as $supportedExtension) {
Assertion::isArray($supportedExtension, 'Invalid Metadata Statement');
$object->supportedExtensions[] = ExtensionDescriptor::createFromArray($supportedExtension);
}
}
$object->rootCertificates = $data['rootCertificates'] ?? [];
if (isset($data['statusReports'])) {
$reports = $data['statusReports'];
Assertion::isArray($reports, 'Invalid Metadata Statement');
foreach ($reports as $report) {
Assertion::isArray($report, 'Invalid Metadata Statement');
$object->statusReports[] = StatusReport::createFromArray($report);
}
}
return $object;
}
public function jsonSerialize(): array
{
$data = [
'legalHeader' => $this->legalHeader,
'aaid' => $this->aaid,
'aaguid' => $this->aaguid,
'attestationCertificateKeyIdentifiers' => $this->attestationCertificateKeyIdentifiers,
'description' => $this->description,
'alternativeDescriptions' => $this->alternativeDescriptions,
'authenticatorVersion' => $this->authenticatorVersion,
'protocolFamily' => $this->protocolFamily,
'upv' => $this->upv,
'assertionScheme' => $this->assertionScheme,
'authenticationAlgorithm' => $this->authenticationAlgorithm,
'authenticationAlgorithms' => $this->authenticationAlgorithms,
'publicKeyAlgAndEncoding' => $this->publicKeyAlgAndEncoding,
'publicKeyAlgAndEncodings' => $this->publicKeyAlgAndEncodings,
'attestationTypes' => $this->attestationTypes,
'userVerificationDetails' => $this->userVerificationDetails,
'keyProtection' => $this->keyProtection,
'isKeyRestricted' => $this->isKeyRestricted,
'isFreshUserVerificationRequired' => $this->isFreshUserVerificationRequired,
'matcherProtection' => $this->matcherProtection,
'cryptoStrength' => $this->cryptoStrength,
'operatingEnv' => $this->operatingEnv,
'attachmentHint' => $this->attachmentHint,
'isSecondFactorOnly' => $this->isSecondFactorOnly,
'tcDisplay' => $this->tcDisplay,
'tcDisplayContentType' => $this->tcDisplayContentType,
'tcDisplayPNGCharacteristics' => array_map(static function (DisplayPNGCharacteristicsDescriptor $object): array {
return $object->jsonSerialize();
}, $this->tcDisplayPNGCharacteristics),
'attestationRootCertificates' => $this->attestationRootCertificates,
'ecdaaTrustAnchors' => array_map(static function (EcdaaTrustAnchor $object): array {
return $object->jsonSerialize();
}, $this->ecdaaTrustAnchors),
'icon' => $this->icon,
'supportedExtensions' => array_map(static function (ExtensionDescriptor $object): array {
return $object->jsonSerialize();
}, $this->supportedExtensions),
'rootCertificates' => $this->rootCertificates,
'statusReports' => $this->statusReports,
];
return Utils::filterNullValues($data);
}
/**
* @return StatusReport[]
*/
public function getStatusReports(): array
{
return $this->statusReports;
}
/**
* @param StatusReport[] $statusReports
*/
public function setStatusReports(array $statusReports): self
{
$this->statusReports = $statusReports;
return $this;
}
/**
* @return string[]
*/
public function getRootCertificates(): array
{
return $this->rootCertificates;
}
/**
* @param string[] $rootCertificates
*/
public function setRootCertificates(array $rootCertificates): self
{
$this->rootCertificates = $rootCertificates;
return $this;
}
}

View file

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use Base64Url\Base64Url;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use function Safe\json_decode;
use function Safe\sprintf;
/**
* @deprecated This class is deprecated since v3.3 and will be removed in v4.0
*/
class MetadataStatementFetcher
{
public static function fetchTableOfContent(string $uri, ClientInterface $client, RequestFactoryInterface $requestFactory, array $additionalHeaders = []): MetadataTOCPayload
{
$content = self::fetch($uri, $client, $requestFactory, $additionalHeaders);
$payload = self::getJwsPayload($content);
$data = json_decode($payload, true);
return MetadataTOCPayload::createFromArray($data);
}
public static function fetchMetadataStatement(string $uri, bool $isBase64UrlEncoded, ClientInterface $client, RequestFactoryInterface $requestFactory, array $additionalHeaders = [], string $hash = '', string $hashingFunction = 'sha256'): MetadataStatement
{
$payload = self::fetch($uri, $client, $requestFactory, $additionalHeaders);
if ('' !== $hash) {
Assertion::true(hash_equals($hash, hash($hashingFunction, $payload, true)), 'The hash cannot be verified. The metadata statement shall be rejected');
}
$json = $isBase64UrlEncoded ? Base64Url::decode($payload) : $payload;
$data = json_decode($json, true);
return MetadataStatement::createFromArray($data);
}
private static function fetch(string $uri, ClientInterface $client, RequestFactoryInterface $requestFactory, array $additionalHeaders = []): string
{
$request = $requestFactory->createRequest('GET', $uri);
foreach ($additionalHeaders as $k => $v) {
$request = $request->withHeader($k, $v);
}
$response = $client->sendRequest($request);
Assertion::eq(200, $response->getStatusCode(), sprintf('Unable to contact the server. Response code is %d', $response->getStatusCode()));
$content = $response->getBody()->getContents();
Assertion::notEmpty($content, 'Unable to contact the server. The response has no content');
return $content;
}
private static function getJwsPayload(string $token): string
{
$jws = (new CompactSerializer())->unserialize($token);
Assertion::eq(1, $jws->countSignatures(), 'Invalid response from the metadata service. Only one signature shall be present.');
$signature = $jws->getSignature(0);
$payload = $jws->getPayload();
Assertion::notEmpty($payload, 'Invalid response from the metadata service. The token payload is empty.');
$header = $signature->getProtectedHeader();
Assertion::keyExists($header, 'alg', 'The "alg" parameter is missing.');
Assertion::eq($header['alg'], 'ES256', 'The expected "alg" parameter value should be "ES256".');
Assertion::keyExists($header, 'x5c', 'The "x5c" parameter is missing.');
Assertion::isArray($header['x5c'], 'The "x5c" parameter should be an array.');
$key = JWKFactory::createFromX5C($header['x5c']);
$algorithm = new ES256();
$isValid = $algorithm->verify($key, $signature->getEncodedProtectedHeader().'.'.$jws->getEncodedPayload(), $signature->getSignature());
Assertion::true($isValid, 'Invalid response from the metadata service. The token signature is invalid.');
return $jws->getPayload();
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
interface MetadataStatementRepository
{
public function findOneByAAGUID(string $aaguid): ?MetadataStatement;
/**
* @deprecated This method is deprecated since v3.3 and will be removed in v4.0. Please use the method "getStatusReports()" provided by the MetadataStatement object
*
* @return StatusReport[]
*/
public function findStatusReportsByAAGUID(string $aaguid): array;
}

View file

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use function array_key_exists;
use Assert\Assertion;
use JsonSerializable;
use function Safe\sprintf;
class MetadataTOCPayload implements JsonSerializable
{
/**
* @var string|null
*/
private $legalHeader;
/**
* @var int
*/
private $no;
/**
* @var string
*/
private $nextUpdate;
/**
* @var MetadataTOCPayloadEntry[]
*/
private $entries = [];
/**
* @var string[]
*/
private $rootCertificates;
public function __construct(int $no, string $nextUpdate, ?string $legalHeader = null)
{
$this->no = $no;
$this->nextUpdate = $nextUpdate;
$this->legalHeader = $legalHeader;
}
public function addEntry(MetadataTOCPayloadEntry $entry): self
{
$this->entries[] = $entry;
return $this;
}
public function getLegalHeader(): ?string
{
return $this->legalHeader;
}
public function getNo(): int
{
return $this->no;
}
public function getNextUpdate(): string
{
return $this->nextUpdate;
}
/**
* @return MetadataTOCPayloadEntry[]
*/
public function getEntries(): array
{
return $this->entries;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['no', 'nextUpdate', 'entries'] as $key) {
Assertion::keyExists($data, $key, Utils::logicException(sprintf('Invalid data. The parameter "%s" is missing', $key)));
}
Assertion::integer($data['no'], Utils::logicException('Invalid data. The parameter "no" shall be an integer'));
Assertion::string($data['nextUpdate'], Utils::logicException('Invalid data. The parameter "nextUpdate" shall be a string'));
Assertion::isArray($data['entries'], Utils::logicException('Invalid data. The parameter "entries" shall be a n array of entries'));
if (array_key_exists('legalHeader', $data)) {
Assertion::string($data['legalHeader'], Utils::logicException('Invalid data. The parameter "legalHeader" shall be a string'));
}
$object = new self(
$data['no'],
$data['nextUpdate'],
$data['legalHeader'] ?? null
);
foreach ($data['entries'] as $k => $entry) {
$object->addEntry(MetadataTOCPayloadEntry::createFromArray($entry));
}
$object->rootCertificates = $data['rootCertificates'] ?? [];
return $object;
}
public function jsonSerialize(): array
{
$data = [
'legalHeader' => $this->legalHeader,
'nextUpdate' => $this->nextUpdate,
'no' => $this->no,
'entries' => array_map(static function (MetadataTOCPayloadEntry $object): array {
return $object->jsonSerialize();
}, $this->entries),
'rootCertificates' => $this->rootCertificates,
];
return Utils::filterNullValues($data);
}
/**
* @return string[]
*/
public function getRootCertificates(): array
{
return $this->rootCertificates;
}
/**
* @param string[] $rootCertificates
*/
public function setRootCertificates(array $rootCertificates): self
{
$this->rootCertificates = $rootCertificates;
return $this;
}
}

View file

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use Base64Url\Base64Url;
use function count;
use JsonSerializable;
use LogicException;
class MetadataTOCPayloadEntry implements JsonSerializable
{
/**
* @var string|null
*/
private $aaid;
/**
* @var string|null
*/
private $aaguid;
/**
* @var string[]
*/
private $attestationCertificateKeyIdentifiers = [];
/**
* @var string|null
*/
private $hash;
/**
* @var string|null
*/
private $url;
/**
* @var StatusReport[]
*/
private $statusReports = [];
/**
* @var string
*/
private $timeOfLastStatusChange;
/**
* @var string
*/
private $rogueListURL;
/**
* @var string
*/
private $rogueListHash;
public function __construct(?string $aaid, ?string $aaguid, array $attestationCertificateKeyIdentifiers, ?string $hash, ?string $url, string $timeOfLastStatusChange, ?string $rogueListURL, ?string $rogueListHash)
{
if (null !== $aaid && null !== $aaguid) {
throw new LogicException('Authenticators cannot support both AAID and AAGUID');
}
if (null === $aaid && null === $aaguid && 0 === count($attestationCertificateKeyIdentifiers)) {
throw new LogicException('If neither AAID nor AAGUID are set, the attestation certificate identifier list shall not be empty');
}
foreach ($attestationCertificateKeyIdentifiers as $attestationCertificateKeyIdentifier) {
Assertion::string($attestationCertificateKeyIdentifier, Utils::logicException('Invalid attestation certificate identifier. Shall be a list of strings'));
Assertion::notEmpty($attestationCertificateKeyIdentifier, Utils::logicException('Invalid attestation certificate identifier. Shall be a list of strings'));
Assertion::regex($attestationCertificateKeyIdentifier, '/^[0-9a-f]+$/', Utils::logicException('Invalid attestation certificate identifier. Shall be a list of strings'));
}
$this->aaid = $aaid;
$this->aaguid = $aaguid;
$this->attestationCertificateKeyIdentifiers = $attestationCertificateKeyIdentifiers;
$this->hash = Base64Url::decode($hash);
$this->url = $url;
$this->timeOfLastStatusChange = $timeOfLastStatusChange;
$this->rogueListURL = $rogueListURL;
$this->rogueListHash = $rogueListHash;
}
public function getAaid(): ?string
{
return $this->aaid;
}
public function getAaguid(): ?string
{
return $this->aaguid;
}
public function getAttestationCertificateKeyIdentifiers(): array
{
return $this->attestationCertificateKeyIdentifiers;
}
public function getHash(): ?string
{
return $this->hash;
}
public function getUrl(): ?string
{
return $this->url;
}
public function addStatusReports(StatusReport $statusReport): self
{
$this->statusReports[] = $statusReport;
return $this;
}
/**
* @return StatusReport[]
*/
public function getStatusReports(): array
{
return $this->statusReports;
}
public function getTimeOfLastStatusChange(): string
{
return $this->timeOfLastStatusChange;
}
public function getRogueListURL(): string
{
return $this->rogueListURL;
}
public function getRogueListHash(): string
{
return $this->rogueListHash;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
Assertion::keyExists($data, 'timeOfLastStatusChange', Utils::logicException('Invalid data. The parameter "timeOfLastStatusChange" is missing'));
Assertion::keyExists($data, 'statusReports', Utils::logicException('Invalid data. The parameter "statusReports" is missing'));
Assertion::isArray($data['statusReports'], Utils::logicException('Invalid data. The parameter "statusReports" shall be an array of StatusReport objects'));
$object = new self(
$data['aaid'] ?? null,
$data['aaguid'] ?? null,
$data['attestationCertificateKeyIdentifiers'] ?? [],
$data['hash'] ?? null,
$data['url'] ?? null,
$data['timeOfLastStatusChange'],
$data['rogueListURL'] ?? null,
$data['rogueListHash'] ?? null
);
foreach ($data['statusReports'] as $statusReport) {
$object->addStatusReports(StatusReport::createFromArray($statusReport));
}
return $object;
}
public function jsonSerialize(): array
{
$data = [
'aaid' => $this->aaid,
'aaguid' => $this->aaguid,
'attestationCertificateKeyIdentifiers' => $this->attestationCertificateKeyIdentifiers,
'hash' => Base64Url::encode($this->hash),
'url' => $this->url,
'statusReports' => array_map(static function (StatusReport $object): array {
return $object->jsonSerialize();
}, $this->statusReports),
'timeOfLastStatusChange' => $this->timeOfLastStatusChange,
'rogueListURL' => $this->rogueListURL,
'rogueListHash' => $this->rogueListHash,
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use function array_key_exists;
use Assert\Assertion;
use function Safe\sprintf;
class PatternAccuracyDescriptor extends AbstractDescriptor
{
/**
* @var int
*/
private $minComplexity;
public function __construct(int $minComplexity, ?int $maxRetries = null, ?int $blockSlowdown = null)
{
Assertion::greaterOrEqualThan($minComplexity, 0, Utils::logicException('Invalid data. The value of "minComplexity" must be a positive integer'));
$this->minComplexity = $minComplexity;
parent::__construct($maxRetries, $blockSlowdown);
}
public function getMinComplexity(): int
{
return $this->minComplexity;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
Assertion::keyExists($data, 'minComplexity', Utils::logicException('The key "minComplexity" is missing'));
foreach (['minComplexity', 'maxRetries', 'blockSlowdown'] as $key) {
if (array_key_exists($key, $data)) {
Assertion::integer($data[$key], Utils::logicException(sprintf('Invalid data. The value of "%s" must be a positive integer', $key)));
}
}
return new self(
$data['minComplexity'],
$data['maxRetries'] ?? null,
$data['blockSlowdown'] ?? null
);
}
public function jsonSerialize(): array
{
$data = [
'minComplexity' => $this->minComplexity,
'maxRetries' => $this->getMaxRetries(),
'blockSlowdown' => $this->getBlockSlowdown(),
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use JsonSerializable;
use function Safe\sprintf;
class RgbPaletteEntry implements JsonSerializable
{
/**
* @var int
*/
private $r;
/**
* @var int
*/
private $g;
/**
* @var int
*/
private $b;
public function __construct(int $r, int $g, int $b)
{
Assertion::range($r, 0, 255, Utils::logicException('The key "r" is invalid'));
Assertion::range($g, 0, 255, Utils::logicException('The key "g" is invalid'));
Assertion::range($b, 0, 255, Utils::logicException('The key "b" is invalid'));
$this->r = $r;
$this->g = $g;
$this->b = $b;
}
public function getR(): int
{
return $this->r;
}
public function getG(): int
{
return $this->g;
}
public function getB(): int
{
return $this->b;
}
public static function createFromArray(array $data): self
{
foreach (['r', 'g', 'b'] as $key) {
Assertion::keyExists($data, $key, sprintf('The key "%s" is missing', $key));
Assertion::integer($data[$key], sprintf('The key "%s" is invalid', $key));
}
return new self(
$data['r'],
$data['g'],
$data['b']
);
}
public function jsonSerialize(): array
{
return [
'r' => $this->r,
'g' => $this->g,
'b' => $this->b,
];
}
}

View file

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use JsonSerializable;
class RogueListEntry implements JsonSerializable
{
/**
* @var string
*/
private $sk;
/**
* @var string
*/
private $date;
public function __construct(string $sk, string $date)
{
$this->sk = $sk;
$this->date = $date;
}
public function getSk(): string
{
return $this->sk;
}
public function getDate(): ?string
{
return $this->date;
}
public static function createFromArray(array $data): self
{
Assertion::keyExists($data, 'sk', 'The key "sk" is missing');
Assertion::string($data['sk'], 'The key "sk" is invalid');
Assertion::keyExists($data, 'date', 'The key "date" is missing');
Assertion::string($data['date'], 'The key "date" is invalid');
return new self(
$data['sk'],
$data['date']
);
}
public function jsonSerialize(): array
{
return [
'sk' => $this->sk,
'date' => $this->date,
];
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use function Safe\base64_decode;
use function Safe\json_decode;
class SingleMetadata
{
/**
* @var MetadataStatement
*/
private $statement;
/**
* @var string
*/
private $data;
/**
* @var bool
*/
private $isBase64Encoded;
public function __construct(string $data, bool $isBase64Encoded)
{
$this->data = $data;
$this->isBase64Encoded = $isBase64Encoded;
}
public function getMetadataStatement(): MetadataStatement
{
if (null === $this->statement) {
$json = $this->data;
if ($this->isBase64Encoded) {
$json = base64_decode($this->data, true);
}
$statement = json_decode($json, true);
$this->statement = MetadataStatement::createFromArray($statement);
}
return $this->statement;
}
}

View file

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use function in_array;
use JsonSerializable;
use function Safe\sprintf;
class StatusReport implements JsonSerializable
{
/**
* @var string
*
* @see AuthenticatorStatus
*/
private $status;
/**
* @var string|null
*/
private $effectiveDate;
/**
* @var string|null
*/
private $certificate;
/**
* @var string|null
*/
private $url;
/**
* @var string|null
*/
private $certificationDescriptor;
/**
* @var string|null
*/
private $certificateNumber;
/**
* @var string|null
*/
private $certificationPolicyVersion;
/**
* @var string|null
*/
private $certificationRequirementsVersion;
public function __construct(string $status, ?string $effectiveDate, ?string $certificate, ?string $url, ?string $certificationDescriptor, ?string $certificateNumber, ?string $certificationPolicyVersion, ?string $certificationRequirementsVersion)
{
Assertion::inArray($status, AuthenticatorStatus::list(), Utils::logicException('The value of the key "status" is not acceptable'));
$this->status = $status;
$this->effectiveDate = $effectiveDate;
$this->certificate = $certificate;
$this->url = $url;
$this->certificationDescriptor = $certificationDescriptor;
$this->certificateNumber = $certificateNumber;
$this->certificationPolicyVersion = $certificationPolicyVersion;
$this->certificationRequirementsVersion = $certificationRequirementsVersion;
}
public function isCompromised(): bool
{
return in_array($this->status, [
AuthenticatorStatus::ATTESTATION_KEY_COMPROMISE,
AuthenticatorStatus::USER_KEY_PHYSICAL_COMPROMISE,
AuthenticatorStatus::USER_KEY_REMOTE_COMPROMISE,
AuthenticatorStatus::USER_VERIFICATION_BYPASS,
], true);
}
public function getStatus(): string
{
return $this->status;
}
public function getEffectiveDate(): ?string
{
return $this->effectiveDate;
}
public function getCertificate(): ?string
{
return $this->certificate;
}
public function getUrl(): ?string
{
return $this->url;
}
public function getCertificationDescriptor(): ?string
{
return $this->certificationDescriptor;
}
public function getCertificateNumber(): ?string
{
return $this->certificateNumber;
}
public function getCertificationPolicyVersion(): ?string
{
return $this->certificationPolicyVersion;
}
public function getCertificationRequirementsVersion(): ?string
{
return $this->certificationRequirementsVersion;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
Assertion::keyExists($data, 'status', Utils::logicException('The key "status" is missing'));
foreach (['effectiveDate', 'certificate', 'url', 'certificationDescriptor', 'certificateNumber', 'certificationPolicyVersion', 'certificationRequirementsVersion'] as $key) {
if (isset($data[$key])) {
Assertion::nullOrString($data[$key], Utils::logicException(sprintf('The value of the key "%s" is invalid', $key)));
}
}
return new self(
$data['status'],
$data['effectiveDate'] ?? null,
$data['certificate'] ?? null,
$data['url'] ?? null,
$data['certificationDescriptor'] ?? null,
$data['certificateNumber'] ?? null,
$data['certificationPolicyVersion'] ?? null,
$data['certificationRequirementsVersion'] ?? null
);
}
public function jsonSerialize(): array
{
$data = [
'status' => $this->status,
'effectiveDate' => $this->effectiveDate,
'certificate' => $this->certificate,
'url' => $this->url,
'certificationDescriptor' => $this->certificationDescriptor,
'certificateNumber' => $this->certificateNumber,
'certificationPolicyVersion' => $this->certificationPolicyVersion,
'certificationRequirementsVersion' => $this->certificationRequirementsVersion,
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use LogicException;
use Throwable;
/**
* @internal
*/
abstract class Utils
{
public static function logicException(string $message, ?Throwable $previousException = null): callable
{
return static function () use ($message, $previousException): LogicException {
return new LogicException($message, 0, $previousException);
};
}
public static function filterNullValues(array $data): array
{
return array_filter($data, static function ($var): bool {return null !== $var; });
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use JsonSerializable;
class VerificationMethodANDCombinations implements JsonSerializable
{
/**
* @var VerificationMethodDescriptor[]
*/
private $verificationMethods = [];
public function addVerificationMethodDescriptor(VerificationMethodDescriptor $verificationMethodDescriptor): self
{
$this->verificationMethods[] = $verificationMethodDescriptor;
return $this;
}
/**
* @return VerificationMethodDescriptor[]
*/
public function getVerificationMethods(): array
{
return $this->verificationMethods;
}
public static function createFromArray(array $data): self
{
$object = new self();
foreach ($data as $datum) {
Assertion::isArray($datum, Utils::logicException('Invalid data'));
$object->addVerificationMethodDescriptor(VerificationMethodDescriptor::createFromArray($datum));
}
return $object;
}
public function jsonSerialize(): array
{
return array_map(static function (VerificationMethodDescriptor $object): array {
return $object->jsonSerialize();
}, $this->verificationMethods);
}
}

View file

@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use Assert\Assertion;
use JsonSerializable;
use function Safe\sprintf;
class VerificationMethodDescriptor implements JsonSerializable
{
public const USER_VERIFY_PRESENCE = 0x00000001;
public const USER_VERIFY_FINGERPRINT = 0x00000002;
public const USER_VERIFY_PASSCODE = 0x00000004;
public const USER_VERIFY_VOICEPRINT = 0x00000008;
public const USER_VERIFY_FACEPRINT = 0x00000010;
public const USER_VERIFY_LOCATION = 0x00000020;
public const USER_VERIFY_EYEPRINT = 0x00000040;
public const USER_VERIFY_PATTERN = 0x00000080;
public const USER_VERIFY_HANDPRINT = 0x00000100;
public const USER_VERIFY_NONE = 0x00000200;
public const USER_VERIFY_ALL = 0x00000400;
/**
* @var int
*/
private $userVerification;
/**
* @var CodeAccuracyDescriptor|null
*/
private $caDesc;
/**
* @var BiometricAccuracyDescriptor|null
*/
private $baDesc;
/**
* @var PatternAccuracyDescriptor|null
*/
private $paDesc;
public function __construct(int $userVerification, ?CodeAccuracyDescriptor $caDesc = null, ?BiometricAccuracyDescriptor $baDesc = null, ?PatternAccuracyDescriptor $paDesc = null)
{
Assertion::greaterOrEqualThan($userVerification, 0, Utils::logicException('The parameter "userVerification" is invalid'));
$this->userVerification = $userVerification;
$this->caDesc = $caDesc;
$this->baDesc = $baDesc;
$this->paDesc = $paDesc;
}
public function getUserVerification(): int
{
return $this->userVerification;
}
public function userPresence(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_PRESENCE);
}
public function fingerprint(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_FINGERPRINT);
}
public function passcode(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_PASSCODE);
}
public function voicePrint(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_VOICEPRINT);
}
public function facePrint(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_FACEPRINT);
}
public function location(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_LOCATION);
}
public function eyePrint(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_EYEPRINT);
}
public function pattern(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_PATTERN);
}
public function handprint(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_HANDPRINT);
}
public function none(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_NONE);
}
public function all(): bool
{
return 0 !== ($this->userVerification & self::USER_VERIFY_ALL);
}
public function getCaDesc(): ?CodeAccuracyDescriptor
{
return $this->caDesc;
}
public function getBaDesc(): ?BiometricAccuracyDescriptor
{
return $this->baDesc;
}
public function getPaDesc(): ?PatternAccuracyDescriptor
{
return $this->paDesc;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
Assertion::keyExists($data, 'userVerification', Utils::logicException('The parameter "userVerification" is missing'));
Assertion::integer($data['userVerification'], Utils::logicException('The parameter "userVerification" is invalid'));
foreach (['caDesc', 'baDesc', 'paDesc'] as $key) {
if (isset($data[$key])) {
Assertion::isArray($data[$key], Utils::logicException(sprintf('Invalid parameter "%s"', $key)));
}
}
return new self(
$data['userVerification'],
isset($data['caDesc']) ? CodeAccuracyDescriptor::createFromArray($data['caDesc']) : null,
isset($data['baDesc']) ? BiometricAccuracyDescriptor::createFromArray($data['baDesc']) : null,
isset($data['paDesc']) ? PatternAccuracyDescriptor::createFromArray($data['paDesc']) : null
);
}
public function jsonSerialize(): array
{
$data = [
'userVerification' => $this->userVerification,
'caDesc' => null === $this->caDesc ? null : $this->caDesc->jsonSerialize(),
'baDesc' => null === $this->baDesc ? null : $this->baDesc->jsonSerialize(),
'paDesc' => null === $this->paDesc ? null : $this->paDesc->jsonSerialize(),
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\MetadataService;
use function array_key_exists;
use Assert\Assertion;
use JsonSerializable;
use LogicException;
use function Safe\sprintf;
class Version implements JsonSerializable
{
/**
* @var int|null
*/
private $major;
/**
* @var int|null
*/
private $minor;
public function __construct(?int $major, ?int $minor)
{
if (null === $major && null === $minor) {
throw new LogicException('Invalid data. Must contain at least one item');
}
Assertion::greaterOrEqualThan($major, 0, Utils::logicException('Invalid argument "major"'));
Assertion::greaterOrEqualThan($minor, 0, Utils::logicException('Invalid argument "minor"'));
$this->major = $major;
$this->minor = $minor;
}
public function getMajor(): ?int
{
return $this->major;
}
public function getMinor(): ?int
{
return $this->minor;
}
public static function createFromArray(array $data): self
{
$data = Utils::filterNullValues($data);
foreach (['major', 'minor'] as $key) {
if (array_key_exists($key, $data)) {
Assertion::integer($data[$key], sprintf('Invalid value for key "%s"', $key));
}
}
return new self(
$data['major'] ?? null,
$data['minor'] ?? null
);
}
public function jsonSerialize(): array
{
$data = [
'major' => $this->major,
'minor' => $this->minor,
];
return Utils::filterNullValues($data);
}
}

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,49 @@
{
"name": "web-auth/webauthn-lib",
"type": "library",
"license": "MIT",
"description": "FIDO2/Webauthn Support For PHP",
"keywords": ["FIDO", "FIDO2", "webauthn"],
"homepage": "https://github.com/web-auth",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},
{
"name": "All contributors",
"homepage": "https://github.com/web-auth/webauthn-library/contributors"
}
],
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-openssl": "*",
"ext-mbstring": "*",
"beberlei/assert": "^3.2",
"fgrosse/phpasn1": "^2.1",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0",
"psr/log": "^1.1",
"ramsey/uuid": "^3.8|^4.0",
"spomky-labs/base64url": "^2.0",
"spomky-labs/cbor-php": "^1.0|^2.0",
"symfony/process": "^3.0|^4.0|^5.0",
"thecodingmachine/safe": "^1.1",
"web-auth/cose-lib": "self.version",
"web-auth/metadata-service": "self.version"
},
"autoload": {
"psr-4": {
"Webauthn\\": "src/"
}
},
"suggest": {
"psr/log-implementation": "Recommended to receive logs from the library",
"web-token/jwt-key-mgmt": "Mandatory for the AndroidSafetyNet Attestation Statement support",
"web-token/jwt-signature-algorithm-rsa": "Mandatory for the AndroidSafetyNet Attestation Statement support",
"web-token/jwt-signature-algorithm-ecdsa": "Recommended for the AndroidSafetyNet Attestation Statement support",
"web-token/jwt-signature-algorithm-eddsa": "Recommended for the AndroidSafetyNet Attestation Statement support"
}
}

View file

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use function count;
use FG\ASN1\ASNObject;
use FG\ASN1\ExplicitlyTaggedObject;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
use function Safe\hex2bin;
use function Safe\openssl_pkey_get_public;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
public function __construct()
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
}
public function name(): string
{
return 'android-key';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['sig', 'x5c', 'alg'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
}
$certificates = $attestation['attStmt']['x5c'];
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::greaterThan(count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
//Decode leaf attestation certificate
$leaf = $certificates[0];
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
$signedData = $authenticatorData->getAuthData().$clientDataJSONHash;
$alg = $attestationStatement->get('alg');
return 1 === openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg));
}
private function checkCertificateAndGetPublicKey(string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData): void
{
$resource = openssl_pkey_get_public($certificate);
$details = openssl_pkey_get_details($resource);
Assertion::isArray($details, 'Unable to read the certificate');
//Check that authData publicKey matches the public key in the attestation certificate
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
Assertion::notNull($publicKeyData, 'No attested public key found');
$publicDataStream = new StringStream($publicKeyData);
$coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
$publicDataStream->close();
$publicKey = Key::createFromData($coseKey);
Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key');
/*---------------------------*/
$certDetails = openssl_x509_parse($certificate);
//Find Android KeyStore Extension with OID “1.3.6.1.4.1.11129.2.1.17” in certificate extensions
Assertion::isArray($certDetails, 'The certificate is not valid');
Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
Assertion::keyExists($certDetails['extensions'], '1.3.6.1.4.1.11129.2.1.17', 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing');
$extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17'];
$extensionAsAsn1 = ASNObject::fromBinary($extension);
Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$objects = $extensionAsAsn1->getChildren();
//Check that attestationChallenge is set to the clientDataHash.
Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid');
//Check that both teeEnforced and softwareEnforced structures dont contain allApplications(600) tag.
Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$softwareEnforcedFlags = $objects[6];
Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$teeEnforcedFlags = $objects[6];
Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
$this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags);
}
private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void
{
foreach ($sequence->getChildren() as $tag) {
Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag');
/* @var ExplicitlyTaggedObject $tag */
Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found');
}
}
}

View file

@ -0,0 +1,292 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use InvalidArgumentException;
use Jose\Component\Core\Algorithm as AlgorithmInterface;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm;
use Jose\Component\Signature\JWS;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use function Safe\json_decode;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\TrustPath\CertificateTrustPath;
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var string|null
*/
private $apiKey;
/**
* @var ClientInterface|null
*/
private $client;
/**
* @var CompactSerializer
*/
private $jwsSerializer;
/**
* @var JWSVerifier|null
*/
private $jwsVerifier;
/**
* @var RequestFactoryInterface|null
*/
private $requestFactory;
/**
* @var int
*/
private $leeway;
/**
* @var int
*/
private $maxAge;
public function __construct(?ClientInterface $client = null, ?string $apiKey = null, ?RequestFactoryInterface $requestFactory = null, ?int $leeway = null, ?int $maxAge = null)
{
if (!class_exists(Algorithm\RS256::class)) {
throw new RuntimeException('The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?');
}
if (!class_exists(JWKFactory::class)) {
throw new RuntimeException('The class Jose\Component\KeyManagement\JWKFactory is missing. Did you forget to install the package web-token/jwt-key-mgmt?');
}
if (null !== $client) {
@trigger_error('The argument "client" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $apiKey) {
@trigger_error('The argument "apiKey" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $requestFactory) {
@trigger_error('The argument "requestFactory" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $maxAge) {
@trigger_error('The argument "maxAge" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "setMaxAge".', E_USER_DEPRECATED);
}
if (null !== $leeway) {
@trigger_error('The argument "leeway" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "setLeeway".', E_USER_DEPRECATED);
}
$this->jwsSerializer = new CompactSerializer();
$this->initJwsVerifier();
//To be removed in 4.0
$this->leeway = $leeway ?? 0;
$this->maxAge = $maxAge ?? 60000;
$this->apiKey = $apiKey;
$this->client = $client;
$this->requestFactory = $requestFactory;
}
public function enableApiVerification(ClientInterface $client, string $apiKey, RequestFactoryInterface $requestFactory): self
{
$this->apiKey = $apiKey;
$this->client = $client;
$this->requestFactory = $requestFactory;
return $this;
}
public function setMaxAge(int $maxAge): self
{
$this->maxAge = $maxAge;
return $this;
}
public function setLeeway(int $leeway): self
{
$this->leeway = $leeway;
return $this;
}
public function name(): string
{
return 'android-safetynet';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['ver', 'response'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
Assertion::notEmpty($attestation['attStmt'][$key], sprintf('The attestation statement value "%s" is empty.', $key));
}
$jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
$jwsHeader = $jws->getSignature(0)->getProtectedHeader();
Assertion::keyExists($jwsHeader, 'x5c', 'The response in the attestation statement must contain a "x5c" header.');
Assertion::notEmpty($jwsHeader['x5c'], 'The "x5c" parameter in the attestation statement response must contain at least one certificate.');
$certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
$attestation['attStmt']['jws'] = $jws;
return AttestationStatement::createBasic(
$this->name(),
$attestation['attStmt'],
new CertificateTrustPath($certificates)
);
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
$firstCertificate = current($certificates);
Assertion::string($firstCertificate, 'No certificate');
$parsedCertificate = openssl_x509_parse($firstCertificate);
Assertion::isArray($parsedCertificate, 'Invalid attestation object');
Assertion::keyExists($parsedCertificate, 'subject', 'Invalid attestation object');
Assertion::keyExists($parsedCertificate['subject'], 'CN', 'Invalid attestation object');
Assertion::eq($parsedCertificate['subject']['CN'], 'attest.android.com', 'Invalid attestation object');
/** @var JWS $jws */
$jws = $attestationStatement->get('jws');
$payload = $jws->getPayload();
$this->validatePayload($payload, $clientDataJSONHash, $authenticatorData);
//Check the signature
$this->validateSignature($jws, $trustPath);
//Check against Google service
$this->validateUsingGoogleApi($attestationStatement);
return true;
}
private function validatePayload(?string $payload, string $clientDataJSONHash, AuthenticatorData $authenticatorData): void
{
Assertion::notNull($payload, 'Invalid attestation object');
$payload = JsonConverter::decode($payload);
Assertion::isArray($payload, 'Invalid attestation object');
Assertion::keyExists($payload, 'nonce', 'Invalid attestation object. "nonce" is missing.');
Assertion::eq($payload['nonce'], base64_encode(hash('sha256', $authenticatorData->getAuthData().$clientDataJSONHash, true)), 'Invalid attestation object. Invalid nonce');
Assertion::keyExists($payload, 'ctsProfileMatch', 'Invalid attestation object. "ctsProfileMatch" is missing.');
Assertion::true($payload['ctsProfileMatch'], 'Invalid attestation object. "ctsProfileMatch" value is false.');
Assertion::keyExists($payload, 'timestampMs', 'Invalid attestation object. Timestamp is missing.');
Assertion::integer($payload['timestampMs'], 'Invalid attestation object. Timestamp shall be an integer.');
$currentTime = time() * 1000;
Assertion::lessOrEqualThan($payload['timestampMs'], $currentTime + $this->leeway, sprintf('Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
Assertion::lessOrEqualThan($currentTime - $payload['timestampMs'], $this->maxAge, sprintf('Invalid attestation object. Too old. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
}
private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void
{
$jwk = JWKFactory::createFromCertificate($trustPath->getCertificates()[0]);
$isValid = $this->jwsVerifier->verifyWithKey($jws, $jwk, 0);
Assertion::true($isValid, 'Invalid response signature');
}
private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
{
if (null === $this->client || null === $this->apiKey || null === $this->requestFactory) {
return;
}
$uri = sprintf('https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s', urlencode($this->apiKey));
$requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
$request = $this->requestFactory->createRequest('POST', $uri);
$request = $request->withHeader('content-type', 'application/json');
$request->getBody()->write($requestBody);
$response = $this->client->sendRequest($request);
$this->checkGoogleApiResponse($response);
$responseBody = $this->getResponseBody($response);
$responseBodyJson = json_decode($responseBody, true);
Assertion::keyExists($responseBodyJson, 'isValidSignature', 'Invalid response.');
Assertion::boolean($responseBodyJson['isValidSignature'], 'Invalid response.');
Assertion::true($responseBodyJson['isValidSignature'], 'Invalid response.');
}
private function getResponseBody(ResponseInterface $response): string
{
$responseBody = '';
$response->getBody()->rewind();
while (true) {
$tmp = $response->getBody()->read(1024);
if ('' === $tmp) {
break;
}
$responseBody .= $tmp;
}
return $responseBody;
}
private function checkGoogleApiResponse(ResponseInterface $response): void
{
Assertion::eq(200, $response->getStatusCode(), 'Request did not succeeded');
Assertion::true($response->hasHeader('content-type'), 'Unrecognized response');
foreach ($response->getHeader('content-type') as $header) {
if (0 === mb_strpos($header, 'application/json')) {
return;
}
}
throw new InvalidArgumentException('Unrecognized response');
}
/**
* @param string[] $certificates
*
* @return string[]
*/
private function convertCertificatesToPem(array $certificates): array
{
foreach ($certificates as $k => $v) {
$certificates[$k] = CertificateToolbox::fixPEMStructure($v);
}
return $certificates;
}
private function initJwsVerifier(): void
{
$algorithmClasses = [
Algorithm\RS256::class, Algorithm\RS384::class, Algorithm\RS512::class,
Algorithm\PS256::class, Algorithm\PS384::class, Algorithm\PS512::class,
Algorithm\ES256::class, Algorithm\ES384::class, Algorithm\ES512::class,
Algorithm\EdDSA::class,
];
/* @var AlgorithmInterface[] $algorithms */
$algorithms = [];
foreach ($algorithmClasses as $algorithm) {
if (class_exists($algorithm)) {
/* @var AlgorithmInterface $algorithm */
$algorithms[] = new $algorithm();
}
}
$algorithmManager = new AlgorithmManager($algorithms);
$this->jwsVerifier = new JWSVerifier($algorithmManager);
}
}

View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use function count;
use function Safe\openssl_pkey_get_public;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
final class AppleAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
public function __construct()
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
}
public function name(): string
{
return 'apple';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['x5c'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
}
$certificates = $attestation['attStmt']['x5c'];
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::greaterThan(count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
return AttestationStatement::createAnonymizationCA($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
//Decode leaf attestation certificate
$leaf = $certificates[0];
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
return true;
}
private function checkCertificateAndGetPublicKey(string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData): void
{
$resource = openssl_pkey_get_public($certificate);
$details = openssl_pkey_get_details($resource);
Assertion::isArray($details, 'Unable to read the certificate');
//Check that authData publicKey matches the public key in the attestation certificate
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
Assertion::notNull($publicKeyData, 'No attested public key found');
$publicDataStream = new StringStream($publicKeyData);
$coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
$publicDataStream->close();
$publicKey = Key::createFromData($coseKey);
Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
//We check the attested key corresponds to the key in the certificate
Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key');
/*---------------------------*/
$certDetails = openssl_x509_parse($certificate);
//Find Apple Extension with OID “1.2.840.113635.100.8.2” in certificate extensions
Assertion::isArray($certDetails, 'The certificate is not valid');
Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
Assertion::keyExists($certDetails['extensions'], '1.2.840.113635.100.8.2', 'The certificate extension "1.2.840.113635.100.8.2" is missing');
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
$nonceToHash = $authenticatorData->getAuthData().$clientDataHash;
$nonce = hash('sha256', $nonceToHash);
//'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object
Assertion::eq('3024a1220420'.$nonce, bin2hex($extension), 'The client data hash is not valid');
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Webauthn\AuthenticatorData;
use Webauthn\MetadataService\MetadataStatement;
class AttestationObject
{
/**
* @var string
*/
private $rawAttestationObject;
/**
* @var AttestationStatement
*/
private $attStmt;
/**
* @var AuthenticatorData
*/
private $authData;
/**
* @var MetadataStatement|null
*/
private $metadataStatement;
public function __construct(string $rawAttestationObject, AttestationStatement $attStmt, AuthenticatorData $authData, ?MetadataStatement $metadataStatement = null)
{
if (null !== $metadataStatement) {
@trigger_error('The argument "metadataStatement" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatement".', E_USER_DEPRECATED);
}
$this->rawAttestationObject = $rawAttestationObject;
$this->attStmt = $attStmt;
$this->authData = $authData;
$this->metadataStatement = $metadataStatement;
}
public function getRawAttestationObject(): string
{
return $this->rawAttestationObject;
}
public function getAttStmt(): AttestationStatement
{
return $this->attStmt;
}
public function setAttStmt(AttestationStatement $attStmt): void
{
$this->attStmt = $attStmt;
}
public function getAuthData(): AuthenticatorData
{
return $this->authData;
}
public function getMetadataStatement(): ?MetadataStatement
{
return $this->metadataStatement;
}
public function setMetadataStatement(MetadataStatement $metadataStatement): self
{
$this->metadataStatement = $metadataStatement;
return $this;
}
}

View file

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use Base64Url\Base64Url;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use function ord;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Ramsey\Uuid\Uuid;
use function Safe\sprintf;
use function Safe\unpack;
use Throwable;
use Webauthn\AttestedCredentialData;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
use Webauthn\AuthenticatorData;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\StringStream;
class AttestationObjectLoader
{
private const FLAG_AT = 0b01000000;
private const FLAG_ED = 0b10000000;
/**
* @var Decoder
*/
private $decoder;
/**
* @var AttestationStatementSupportManager
*/
private $attestationStatementSupportManager;
/**
* @var LoggerInterface|null
*/
private $logger;
public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
{
if (null !== $metadataStatementRepository) {
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.2 and will be removed in 4.0. Please set `null` instead.', E_USER_DEPRECATED);
}
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger" instead.', E_USER_DEPRECATED);
}
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
$this->logger = $logger ?? new NullLogger();
}
public static function create(AttestationStatementSupportManager $attestationStatementSupportManager): self
{
return new self($attestationStatementSupportManager);
}
public function load(string $data): AttestationObject
{
try {
$this->logger->info('Trying to load the data', ['data' => $data]);
$decodedData = Base64Url::decode($data);
$stream = new StringStream($decodedData);
$parsed = $this->decoder->decode($stream);
$this->logger->info('Loading the Attestation Statement');
$attestationObject = $parsed->getNormalizedData();
Assertion::true($stream->isEOF(), 'Invalid attestation object. Presence of extra bytes.');
$stream->close();
Assertion::isArray($attestationObject, 'Invalid attestation object');
Assertion::keyExists($attestationObject, 'authData', 'Invalid attestation object');
Assertion::keyExists($attestationObject, 'fmt', 'Invalid attestation object');
Assertion::keyExists($attestationObject, 'attStmt', 'Invalid attestation object');
$authData = $attestationObject['authData'];
$attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']);
$attestationStatement = $attestationStatementSupport->load($attestationObject);
$this->logger->info('Attestation Statement loaded');
$this->logger->debug('Attestation Statement loaded', ['attestationStatement' => $attestationStatement]);
$authDataStream = new StringStream($authData);
$rp_id_hash = $authDataStream->read(32);
$flags = $authDataStream->read(1);
$signCount = $authDataStream->read(4);
$signCount = unpack('N', $signCount)[1];
$this->logger->debug(sprintf('Signature counter: %d', $signCount));
$attestedCredentialData = null;
if (0 !== (ord($flags) & self::FLAG_AT)) {
$this->logger->info('Attested Credential Data is present');
$aaguid = Uuid::fromBytes($authDataStream->read(16));
$credentialLength = $authDataStream->read(2);
$credentialLength = unpack('n', $credentialLength)[1];
$credentialId = $authDataStream->read($credentialLength);
$credentialPublicKey = $this->decoder->decode($authDataStream);
Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.');
$attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey);
$this->logger->info('Attested Credential Data loaded');
$this->logger->debug('Attested Credential Data loaded', ['at' => $attestedCredentialData]);
}
$extension = null;
if (0 !== (ord($flags) & self::FLAG_ED)) {
$this->logger->info('Extension Data loaded');
$extension = $this->decoder->decode($authDataStream);
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
$this->logger->info('Extension Data loaded');
$this->logger->debug('Extension Data loaded', ['ed' => $extension]);
}
Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.');
$authDataStream->close();
$authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
$attestationObject = new AttestationObject($data, $attestationStatement, $authenticatorData);
$this->logger->info('Attestation Object loaded');
$this->logger->debug('Attestation Object', ['ed' => $attestationObject]);
return $attestationObject;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
}

View file

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use function array_key_exists;
use Assert\Assertion;
use JsonSerializable;
use function Safe\sprintf;
use Webauthn\TrustPath\TrustPath;
use Webauthn\TrustPath\TrustPathLoader;
class AttestationStatement implements JsonSerializable
{
public const TYPE_NONE = 'none';
public const TYPE_BASIC = 'basic';
public const TYPE_SELF = 'self';
public const TYPE_ATTCA = 'attca';
public const TYPE_ECDAA = 'ecdaa';
public const TYPE_ANONCA = 'anonca';
/**
* @var string
*/
private $fmt;
/**
* @var mixed[]
*/
private $attStmt;
/**
* @var TrustPath
*/
private $trustPath;
/**
* @var string
*/
private $type;
/**
* @param mixed[] $attStmt
*/
public function __construct(string $fmt, array $attStmt, string $type, TrustPath $trustPath)
{
$this->fmt = $fmt;
$this->attStmt = $attStmt;
$this->type = $type;
$this->trustPath = $trustPath;
}
/**
* @param mixed[] $attStmt
*/
public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_NONE, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_BASIC, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_SELF, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_ATTCA, $trustPath);
}
/**
* @param mixed[] $attStmt
*/
public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_ECDAA, $trustPath);
}
public static function createAnonymizationCA(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, self::TYPE_ANONCA, $trustPath);
}
public function getFmt(): string
{
return $this->fmt;
}
/**
* @return mixed[]
*/
public function getAttStmt(): array
{
return $this->attStmt;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->attStmt);
}
/**
* @return mixed
*/
public function get(string $key)
{
Assertion::true($this->has($key), sprintf('The attestation statement has no key "%s".', $key));
return $this->attStmt[$key];
}
public function getTrustPath(): TrustPath
{
return $this->trustPath;
}
public function getType(): string
{
return $this->type;
}
/**
* @param mixed[] $data
*/
public static function createFromArray(array $data): self
{
foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) {
Assertion::keyExists($data, $key, sprintf('The key "%s" is missing', $key));
}
return new self(
$data['fmt'],
$data['attStmt'],
$data['type'],
TrustPathLoader::loadTrustPath($data['trustPath'])
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
return [
'fmt' => $this->fmt,
'attStmt' => $this->attStmt,
'trustPath' => $this->trustPath->jsonSerialize(),
'type' => $this->type,
];
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Webauthn\AuthenticatorData;
interface AttestationStatementSupport
{
public function name(): string;
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement;
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool;
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use function array_key_exists;
use Assert\Assertion;
use function Safe\sprintf;
class AttestationStatementSupportManager
{
/**
* @var AttestationStatementSupport[]
*/
private $attestationStatementSupports = [];
public function add(AttestationStatementSupport $attestationStatementSupport): void
{
$this->attestationStatementSupports[$attestationStatementSupport->name()] = $attestationStatementSupport;
}
public function has(string $name): bool
{
return array_key_exists($name, $this->attestationStatementSupports);
}
public function get(string $name): AttestationStatementSupport
{
Assertion::true($this->has($name), sprintf('The attestation statement format "%s" is not supported.', $name));
return $this->attestationStatementSupports[$name];
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Key\Ec2Key;
use InvalidArgumentException;
use function Safe\openssl_pkey_get_public;
use function Safe\sprintf;
use Throwable;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
public function __construct()
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
}
public function name(): string
{
return 'fido-u2f';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['sig', 'x5c'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
}
$certificates = $attestation['attStmt']['x5c'];
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.');
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
reset($certificates);
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
$this->checkCertificate($certificates[0]);
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
Assertion::eq(
$authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
'00000000-0000-0000-0000-000000000000',
'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"'
);
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$dataToVerify = "\0";
$dataToVerify .= $authenticatorData->getRpIdHash();
$dataToVerify .= $clientDataJSONHash;
$dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId();
$dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey());
return 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256);
}
private function extractPublicKey(?string $publicKey): string
{
Assertion::notNull($publicKey, 'The attested credential data does not contain a valid public key.');
$publicKeyStream = new StringStream($publicKey);
$coseKey = $this->decoder->decode($publicKeyStream);
Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.');
$publicKeyStream->close();
Assertion::isInstanceOf($coseKey, MapObject::class, 'The attested credential data does not contain a valid public key.');
$coseKey = $coseKey->getNormalizedData();
$ec2Key = new Ec2Key($coseKey + [Ec2Key::TYPE => 2, Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256]);
return "\x04".$ec2Key->x().$ec2Key->y();
}
private function checkCertificate(string $publicKey): void
{
try {
$resource = openssl_pkey_get_public($publicKey);
$details = openssl_pkey_get_details($resource);
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable);
}
Assertion::isArray($details, 'Invalid certificate or certificate chain');
Assertion::keyExists($details, 'ec', 'Invalid certificate or certificate chain');
Assertion::keyExists($details['ec'], 'curve_name', 'Invalid certificate or certificate chain');
Assertion::eq($details['ec']['curve_name'], 'prime256v1', 'Invalid certificate or certificate chain');
Assertion::keyExists($details['ec'], 'curve_oid', 'Invalid certificate or certificate chain');
Assertion::eq($details['ec']['curve_oid'], '1.2.840.10045.3.1.7', 'Invalid certificate or certificate chain');
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use function count;
use Webauthn\AuthenticatorData;
use Webauthn\TrustPath\EmptyTrustPath;
final class NoneAttestationStatementSupport implements AttestationStatementSupport
{
public function name(): string
{
return 'none';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::noContent($attestation['attStmt'], 'Invalid attestation object');
return AttestationStatement::createNone($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath());
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
return 0 === count($attestationStatement->getAttStmt());
}
}

View file

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use function array_key_exists;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\Signature;
use Cose\Algorithms;
use Cose\Key\Key;
use function in_array;
use InvalidArgumentException;
use function is_array;
use RuntimeException;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
use Webauthn\Util\CoseSignatureFixer;
final class PackedAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var Decoder
*/
private $decoder;
/**
* @var Manager
*/
private $algorithmManager;
public function __construct(Manager $algorithmManager)
{
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->algorithmManager = $algorithmManager;
}
public function name(): string
{
return 'packed';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation['attStmt'], 'sig', 'The attestation statement value "sig" is missing.');
Assertion::keyExists($attestation['attStmt'], 'alg', 'The attestation statement value "alg" is missing.');
Assertion::string($attestation['attStmt']['sig'], 'The attestation statement value "sig" is missing.');
switch (true) {
case array_key_exists('x5c', $attestation['attStmt']):
return $this->loadBasicType($attestation);
case array_key_exists('ecdaaKeyId', $attestation['attStmt']):
return $this->loadEcdaaType($attestation['attStmt']);
default:
return $this->loadEmptyType($attestation);
}
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
switch (true) {
case $trustPath instanceof CertificateTrustPath:
return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData, $trustPath);
case $trustPath instanceof EcdaaKeyIdTrustPath:
return $this->processWithECDAA();
case $trustPath instanceof EmptyTrustPath:
return $this->processWithSelfAttestation($clientDataJSONHash, $attestationStatement, $authenticatorData);
default:
throw new InvalidArgumentException('Unsupported attestation statement');
}
}
/**
* @param mixed[] $attestation
*/
private function loadBasicType(array $attestation): AttestationStatement
{
$certificates = $attestation['attStmt']['x5c'];
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
}
private function loadEcdaaType(array $attestation): AttestationStatement
{
$ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId'];
Assertion::string($ecdaaKeyId, 'The attestation statement value "ecdaaKeyId" is invalid.');
return AttestationStatement::createEcdaa($attestation['fmt'], $attestation['attStmt'], new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId']));
}
/**
* @param mixed[] $attestation
*/
private function loadEmptyType(array $attestation): AttestationStatement
{
return AttestationStatement::createSelf($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath());
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
Assertion::isArray($parsed, 'Invalid certificate');
//Check version
Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
//Check subject field
Assertion::false(!isset($parsed['name']) || false === mb_strpos($parsed['name'], '/OU=Authenticator Attestation'), 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"');
//Check extensions
Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
//Check certificate is not a CA cert
Assertion::false(!isset($parsed['extensions']['basicConstraints']) || 'CA:FALSE' !== $parsed['extensions']['basicConstraints'], 'The Basic Constraints extension must have the CA component set to false');
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential available');
// id-fido-gen-ce-aaguid OID check
Assertion::false(in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($attestedCredentialData->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate');
}
private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData, CertificateTrustPath $trustPath): bool
{
$certificates = $trustPath->getCertificates();
// Check leaf certificate
$this->checkCertificate($certificates[0], $authenticatorData);
// Get the COSE algorithm identifier and the corresponding OpenSSL one
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
// Verification of the signature
$signedData = $authenticatorData->getAuthData().$clientDataJSONHash;
$result = openssl_verify($signedData, $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier);
return 1 === $result;
}
private function processWithECDAA(): bool
{
throw new RuntimeException('ECDAA not supported');
}
private function processWithSelfAttestation(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential available');
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
Assertion::notNull($credentialPublicKey, 'No credential public key available');
$publicKeyStream = new StringStream($credentialPublicKey);
$publicKey = $this->decoder->decode($publicKeyStream);
Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.');
$publicKeyStream->close();
Assertion::isInstanceOf($publicKey, MapObject::class, 'The attested credential data does not contain a valid public key.');
$publicKey = $publicKey->getNormalizedData(false);
$publicKey = new Key($publicKey);
Assertion::eq($publicKey->alg(), (int) $attestationStatement->get('alg'), 'The algorithm of the attestation statement and the key are not identical.');
$dataToVerify = $authenticatorData->getAuthData().$clientDataJSONHash;
$algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg'));
if (!$algorithm instanceof Signature) {
throw new RuntimeException('Invalid algorithm');
}
$signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm);
return $algorithm->verify($dataToVerify, $publicKey, $signature);
}
}

View file

@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AttestationStatement;
use Assert\Assertion;
use Base64Url\Base64Url;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\OkpKey;
use Cose\Key\RsaKey;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_array;
use RuntimeException;
use Safe\DateTimeImmutable;
use function Safe\sprintf;
use function Safe\unpack;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
final class TPMAttestationStatementSupport implements AttestationStatementSupport
{
public function name(): string
{
return 'tpm';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported');
foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
}
Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object');
$certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object');
$pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
$pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit');
$pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true);
$attestedName = $pubAreaAttestedNameAlg.$pubAreaHash;
Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name');
$attestation['attStmt']['parsedCertInfo'] = $certInfo;
$attestation['attStmt']['parsedPubArea'] = $pubArea;
$certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
return AttestationStatement::createAttCA(
$this->name(),
$attestation['attStmt'],
new CertificateTrustPath($certificates)
);
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash;
$attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true);
Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash');
$this->checkUniquePublicKey(
$attestationStatement->get('parsedPubArea')['unique'],
$authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()
);
switch (true) {
case $attestationStatement->getTrustPath() instanceof CertificateTrustPath:
return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData);
case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath:
return $this->processWithECDAA();
default:
throw new InvalidArgumentException('Unsupported attestation statement');
}
}
private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
{
$cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key');
$key = new Key($publicKey->getNormalizedData(false));
switch ($key->type()) {
case Key::TYPE_OKP:
$uniqueFromKey = (new OkpKey($key->getData()))->x();
break;
case Key::TYPE_EC2:
$ec2Key = new Ec2Key($key->getData());
$uniqueFromKey = "\x04".$ec2Key->x().$ec2Key->y();
break;
case Key::TYPE_RSA:
$uniqueFromKey = (new RsaKey($key->getData()))->n();
break;
default:
throw new InvalidArgumentException('Invalid or unsupported key type.');
}
Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value');
}
/**
* @return mixed[]
*/
private function checkCertInfo(string $data): array
{
$certInfo = new StringStream($data);
$magic = $certInfo->read(4);
Assertion::eq('ff544347', bin2hex($magic), 'Invalid attestation object');
$type = $certInfo->read(2);
$qualifiedSignerLength = unpack('n', $certInfo->read(2))[1];
$qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored
$extraDataLength = unpack('n', $certInfo->read(2))[1];
$extraData = $certInfo->read($extraDataLength);
$clockInfo = $certInfo->read(17); //Ignore
$firmwareVersion = $certInfo->read(8);
$attestedNameLength = unpack('n', $certInfo->read(2))[1];
$attestedName = $certInfo->read($attestedNameLength);
$attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1];
$attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore
Assertion::true($certInfo->isEOF(), 'Invalid certificate information. Presence of extra bytes.');
$certInfo->close();
return [
'magic' => $magic,
'type' => $type,
'qualifiedSigner' => $qualifiedSigner,
'extraData' => $extraData,
'clockInfo' => $clockInfo,
'firmwareVersion' => $firmwareVersion,
'attestedName' => $attestedName,
'attestedQualifiedName' => $attestedQualifiedName,
];
}
/**
* @return mixed[]
*/
private function checkPubArea(string $data): array
{
$pubArea = new StringStream($data);
$type = $pubArea->read(2);
$nameAlg = $pubArea->read(2);
$objectAttributes = $pubArea->read(4);
$authPolicyLength = unpack('n', $pubArea->read(2))[1];
$authPolicy = $pubArea->read($authPolicyLength);
$parameters = $this->getParameters($type, $pubArea);
$uniqueLength = unpack('n', $pubArea->read(2))[1];
$unique = $pubArea->read($uniqueLength);
Assertion::true($pubArea->isEOF(), 'Invalid public area. Presence of extra bytes.');
$pubArea->close();
return [
'type' => $type,
'nameAlg' => $nameAlg,
'objectAttributes' => $objectAttributes,
'authPolicy' => $authPolicy,
'parameters' => $parameters,
'unique' => $unique,
];
}
/**
* @return mixed[]
*/
private function getParameters(string $type, StringStream $stream): array
{
switch (bin2hex($type)) {
case '0001':
case '0014':
case '0016':
return [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'keyBits' => unpack('n', $stream->read(2))[1],
'exponent' => $this->getExponent($stream->read(4)),
];
case '0018':
return [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'curveId' => $stream->read(2),
'kdf' => $stream->read(2),
];
default:
throw new InvalidArgumentException('Unsupported type');
}
}
private function getExponent(string $exponent): string
{
return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent;
}
private function getTPMHash(string $nameAlg): string
{
switch (bin2hex($nameAlg)) {
case '0004':
return 'sha1'; //: "TPM_ALG_SHA1",
case '000b':
return 'sha256'; //: "TPM_ALG_SHA256",
case '000c':
return 'sha384'; //: "TPM_ALG_SHA384",
case '000d':
return 'sha512'; //: "TPM_ALG_SHA512",
default:
throw new InvalidArgumentException('Unsupported hash algorithm');
}
}
private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
// Check certificate CA chain and returns the Attestation Certificate
$this->checkCertificate($certificates[0], $authenticatorData);
// Get the COSE algorithm identifier and the corresponding OpenSSL one
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
$result = openssl_verify($attestationStatement->get('certInfo'), $attestationStatement->get('sig'), $certificates[0], $opensslAlgorithmIdentifier);
return 1 === $result;
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
Assertion::isArray($parsed, 'Invalid certificate');
//Check version
Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
//Check subject field is empty
Assertion::false(!isset($parsed['subject']) || !is_array($parsed['subject']) || 0 !== count($parsed['subject']), 'Invalid certificate name. The Subject should be empty');
// Check period of validity
Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.');
Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.');
$startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.');
Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.');
Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.');
$endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.');
//Check extensions
Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
//Check subjectAltName
Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing');
//Check extendedKeyUsage
Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing');
Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid');
// id-fido-gen-ce-aaguid OID check
Assertion::false(in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($authenticatorData->getAttestedCredentialData()->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate');
}
private function processWithECDAA(): bool
{
throw new RuntimeException('ECDAA not supported');
}
}

View file

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use JsonSerializable;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function Safe\base64_decode;
/**
* @see https://www.w3.org/TR/webauthn/#sec-attested-credential-data
*/
class AttestedCredentialData implements JsonSerializable
{
/**
* @var UuidInterface
*/
private $aaguid;
/**
* @var string
*/
private $credentialId;
/**
* @var string|null
*/
private $credentialPublicKey;
public function __construct(UuidInterface $aaguid, string $credentialId, ?string $credentialPublicKey)
{
$this->aaguid = $aaguid;
$this->credentialId = $credentialId;
$this->credentialPublicKey = $credentialPublicKey;
}
public function getAaguid(): UuidInterface
{
return $this->aaguid;
}
public function setAaguid(UuidInterface $aaguid): void
{
$this->aaguid = $aaguid;
}
public function getCredentialId(): string
{
return $this->credentialId;
}
public function getCredentialPublicKey(): ?string
{
return $this->credentialPublicKey;
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
Assertion::keyExists($json, 'aaguid', 'Invalid input. "aaguid" is missing.');
Assertion::keyExists($json, 'credentialId', 'Invalid input. "credentialId" is missing.');
switch (true) {
case 36 === mb_strlen($json['aaguid'], '8bit'):
$uuid = Uuid::fromString($json['aaguid']);
break;
default: // Kept for compatibility with old format
$decoded = base64_decode($json['aaguid'], true);
$uuid = Uuid::fromBytes($decoded);
}
$credentialId = base64_decode($json['credentialId'], true);
$credentialPublicKey = null;
if (isset($json['credentialPublicKey'])) {
$credentialPublicKey = base64_decode($json['credentialPublicKey'], true);
}
return new self(
$uuid,
$credentialId,
$credentialPublicKey
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$result = [
'aaguid' => $this->aaguid->toString(),
'credentialId' => base64_encode($this->credentialId),
];
if (null !== $this->credentialPublicKey) {
$result['credentialPublicKey'] = base64_encode($this->credentialPublicKey);
}
return $result;
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use JsonSerializable;
class AuthenticationExtension implements JsonSerializable
{
/**
* @var string
*/
private $name;
/**
* @var mixed
*/
private $value;
/**
* @param mixed $value
*/
public function __construct(string $name, $value)
{
$this->name = $name;
$this->value = $value;
}
public function name(): string
{
return $this->name;
}
/**
* @return mixed
*/
public function value()
{
return $this->value;
}
/**
* @return mixed
*/
public function jsonSerialize()
{
return $this->value;
}
}

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use function array_key_exists;
use ArrayIterator;
use Assert\Assertion;
use function count;
use Countable;
use Iterator;
use IteratorAggregate;
use JsonSerializable;
use function Safe\sprintf;
class AuthenticationExtensionsClientInputs implements JsonSerializable, Countable, IteratorAggregate
{
/**
* @var AuthenticationExtension[]
*/
private $extensions = [];
public function add(AuthenticationExtension $extension): void
{
$this->extensions[$extension->name()] = $extension;
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
$object = new self();
foreach ($json as $k => $v) {
$object->add(new AuthenticationExtension($k, $v));
}
return $object;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->extensions);
}
/**
* @return mixed
*/
public function get(string $key)
{
Assertion::true($this->has($key), sprintf('The extension with key "%s" is not available', $key));
return $this->extensions[$key];
}
/**
* @return AuthenticationExtension[]
*/
public function jsonSerialize(): array
{
return array_map(static function (AuthenticationExtension $object) {
return $object->jsonSerialize();
}, $this->extensions);
}
/**
* @return Iterator<string, AuthenticationExtension>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->extensions);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->extensions, $mode);
}
}

View file

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use function array_key_exists;
use ArrayIterator;
use Assert\Assertion;
use function count;
use Countable;
use Iterator;
use IteratorAggregate;
use JsonSerializable;
use function Safe\json_decode;
use function Safe\sprintf;
class AuthenticationExtensionsClientOutputs implements JsonSerializable, Countable, IteratorAggregate
{
/**
* @var AuthenticationExtension[]
*/
private $extensions = [];
public function add(AuthenticationExtension $extension): void
{
$this->extensions[$extension->name()] = $extension;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
$object = new self();
foreach ($json as $k => $v) {
$object->add(new AuthenticationExtension($k, $v));
}
return $object;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->extensions);
}
/**
* @return mixed
*/
public function get(string $key)
{
Assertion::true($this->has($key), sprintf('The extension with key "%s" is not available', $key));
return $this->extensions[$key];
}
/**
* @return AuthenticationExtension[]
*/
public function jsonSerialize(): array
{
return array_map(static function (AuthenticationExtension $object) {
return $object->jsonSerialize();
}, $this->extensions);
}
/**
* @return Iterator<string, AuthenticationExtension>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->extensions);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->extensions, $mode);
}
}

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use Assert\Assertion;
use CBOR\CBORObject;
use CBOR\MapObject;
abstract class AuthenticationExtensionsClientOutputsLoader
{
public static function load(CBORObject $object): AuthenticationExtensionsClientOutputs
{
Assertion::isInstanceOf($object, MapObject::class, 'Invalid extension object');
$data = $object->getNormalizedData();
$extensions = new AuthenticationExtensionsClientOutputs();
foreach ($data as $key => $value) {
Assertion::string($key, 'Invalid extension key');
$extensions->add(new AuthenticationExtension($key, $value));
}
return $extensions;
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
interface ExtensionOutputChecker
{
/**
* @throws ExtensionOutputError
*/
public function check(AuthenticationExtensionsClientInputs $inputs, AuthenticationExtensionsClientOutputs $outputs): void;
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
class ExtensionOutputCheckerHandler
{
/**
* @var ExtensionOutputChecker[]
*/
private $checkers = [];
public function add(ExtensionOutputChecker $checker): void
{
$this->checkers[] = $checker;
}
/**
* @throws ExtensionOutputError
*/
public function check(AuthenticationExtensionsClientInputs $inputs, AuthenticationExtensionsClientOutputs $outputs): void
{
foreach ($this->checkers as $checker) {
$checker->check($inputs, $outputs);
}
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\AuthenticationExtensions;
use Exception;
use Throwable;
class ExtensionOutputError extends Exception
{
/**
* @var AuthenticationExtension
*/
private $authenticationExtension;
public function __construct(AuthenticationExtension $authenticationExtension, string $message = '', int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->authenticationExtension = $authenticationExtension;
}
public function getAuthenticationExtension(): AuthenticationExtension
{
return $this->authenticationExtension;
}
}

View file

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function Safe\base64_decode;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
*/
class AuthenticatorAssertionResponse extends AuthenticatorResponse
{
/**
* @var AuthenticatorData
*/
private $authenticatorData;
/**
* @var string
*/
private $signature;
/**
* @var string|null
*/
private $userHandle;
public function __construct(CollectedClientData $clientDataJSON, AuthenticatorData $authenticatorData, string $signature, ?string $userHandle)
{
parent::__construct($clientDataJSON);
$this->authenticatorData = $authenticatorData;
$this->signature = $signature;
$this->userHandle = $userHandle;
}
public function getAuthenticatorData(): AuthenticatorData
{
return $this->authenticatorData;
}
public function getSignature(): string
{
return $this->signature;
}
public function getUserHandle(): ?string
{
if (null === $this->userHandle || '' === $this->userHandle) {
return $this->userHandle;
}
return base64_decode($this->userHandle, true);
}
}

View file

@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\Signature;
use Cose\Key\Key;
use function count;
use function in_array;
use function is_string;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use function Safe\parse_url;
use Throwable;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\Counter\CounterChecker;
use Webauthn\Counter\ThrowExceptionIfInvalid;
use Webauthn\TokenBinding\TokenBindingHandler;
use Webauthn\Util\CoseSignatureFixer;
class AuthenticatorAssertionResponseValidator
{
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSourceRepository;
/**
* @var Decoder
*/
private $decoder;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var Manager|null
*/
private $algorithmManager;
/**
* @var CounterChecker
*/
private $counterChecker;
/**
* @var LoggerInterface|null
*/
private $logger;
public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, Manager $algorithmManager, ?CounterChecker $counterChecker = null, ?LoggerInterface $logger = null)
{
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
}
if (null !== $counterChecker) {
@trigger_error('The argument "counterChecker" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setCounterChecker".', E_USER_DEPRECATED);
}
$this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->tokenBindingHandler = $tokenBindingHandler;
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
$this->algorithmManager = $algorithmManager;
$this->counterChecker = $counterChecker ?? new ThrowExceptionIfInvalid();
$this->logger = $logger ?? new NullLogger();
}
/**
* @see https://www.w3.org/TR/webauthn/#verifying-assertion
*/
public function check(string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface $request, ?string $userHandle, array $securedRelyingPartyId = []): PublicKeyCredentialSource
{
try {
$this->logger->info('Checking the authenticator assertion response', [
'credentialId' => $credentialId,
'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
'host' => $request->getUri()->getHost(),
'userHandle' => $userHandle,
]);
/** @see 7.2.1 */
if (0 !== count($publicKeyCredentialRequestOptions->getAllowCredentials())) {
Assertion::true($this->isCredentialIdAllowed($credentialId, $publicKeyCredentialRequestOptions->getAllowCredentials()), 'The credential ID is not allowed.');
}
/** @see 7.2.2 */
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($credentialId);
Assertion::notNull($publicKeyCredentialSource, 'The credential ID is invalid.');
/** @see 7.2.3 */
$attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
$credentialUserHandle = $publicKeyCredentialSource->getUserHandle();
$responseUserHandle = $authenticatorAssertionResponse->getUserHandle();
/** @see 7.2.2 User Handle*/
if (null !== $userHandle) { //If the user was identified before the authentication ceremony was initiated,
Assertion::eq($credentialUserHandle, $userHandle, 'Invalid user handle');
if (null !== $responseUserHandle && '' !== $responseUserHandle) {
Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
}
} else {
Assertion::notEmpty($responseUserHandle, 'User handle is mandatory');
Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
}
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
if ($isU2F) {
$credentialPublicKey = U2FPublicKey::createCOSEKey($credentialPublicKey);
}
Assertion::notNull($credentialPublicKey, 'No public key available.');
$stream = new StringStream($credentialPublicKey);
$credentialPublicKeyStream = $this->decoder->decode($stream);
Assertion::true($stream->isEOF(), 'Invalid key. Presence of extra bytes.');
$stream->close();
/** @see 7.2.4 */
/** @see 7.2.5 */
//Nothing to do. Use of objects directly
/** @see 7.2.6 */
$C = $authenticatorAssertionResponse->getClientDataJSON();
/** @see 7.2.7 */
Assertion::eq('webauthn.get', $C->getType(), 'The client data type is not "webauthn.get".');
/** @see 7.2.8 */
Assertion::true(hash_equals($publicKeyCredentialRequestOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
/** @see 7.2.9 */
$rpId = $publicKeyCredentialRequestOptions->getRpId() ?? $request->getUri()->getHost();
$facetId = $this->getFacetId($rpId, $publicKeyCredentialRequestOptions->getExtensions(), $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions());
$parsedRelyingPartyId = parse_url($C->getOrigin());
Assertion::isArray($parsedRelyingPartyId, 'Invalid origin');
if (!in_array($facetId, $securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
}
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
/** @see 7.2.10 */
if (null !== $C->getTokenBinding()) {
$this->tokenBindingHandler->check($C->getTokenBinding(), $request);
}
$expectedRpIdHash = $isU2F ? $C->getOrigin() : $facetId;
// u2f response has full origin in rpIdHash
/** @see 7.2.11 */
$rpIdHash = hash('sha256', $expectedRpIdHash, true);
Assertion::true(hash_equals($rpIdHash, $authenticatorAssertionResponse->getAuthenticatorData()->getRpIdHash()), 'rpId hash mismatch.');
/** @see 7.2.12 */
Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserPresent(), 'User was not present');
/** @see 7.2.13 */
if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialRequestOptions->getUserVerification()) {
Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserVerified(), 'User authentication required.');
}
/** @see 7.2.14 */
$extensionsClientOutputs = $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions();
if (null !== $extensionsClientOutputs) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialRequestOptions->getExtensions(),
$extensionsClientOutputs
);
}
/** @see 7.2.15 */
$getClientDataJSONHash = hash('sha256', $authenticatorAssertionResponse->getClientDataJSON()->getRawData(), true);
/** @see 7.2.16 */
$dataToVerify = $authenticatorAssertionResponse->getAuthenticatorData()->getAuthData().$getClientDataJSONHash;
$signature = $authenticatorAssertionResponse->getSignature();
$coseKey = new Key($credentialPublicKeyStream->getNormalizedData());
$algorithm = $this->algorithmManager->get($coseKey->alg());
Assertion::isInstanceOf($algorithm, Signature::class, 'Invalid algorithm identifier. Should refer to a signature algorithm');
$signature = CoseSignatureFixer::fix($signature, $algorithm);
Assertion::true($algorithm->verify($dataToVerify, $coseKey, $signature), 'Invalid signature.');
/** @see 7.2.17 */
$storedCounter = $publicKeyCredentialSource->getCounter();
$responseCounter = $authenticatorAssertionResponse->getAuthenticatorData()->getSignCount();
if (0 !== $responseCounter || 0 !== $storedCounter) {
$this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
}
$publicKeyCredentialSource->setCounter($responseCounter);
$this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
/** @see 7.2.18 */
//All good. We can continue.
$this->logger->info('The assertion is valid');
$this->logger->debug('Public Key Credential Source', ['publicKeyCredentialSource' => $publicKeyCredentialSource]);
return $publicKeyCredentialSource;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function setCounterChecker(CounterChecker $counterChecker): self
{
$this->counterChecker = $counterChecker;
return $this;
}
/**
* @param array<PublicKeyCredentialDescriptor> $allowedCredentials
*/
private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool
{
foreach ($allowedCredentials as $allowedCredential) {
if (hash_equals($allowedCredential->getId(), $credentialId)) {
return true;
}
}
return false;
}
private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
{
if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')->value();
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
if (!is_string($appId) || true !== $wasUsed) {
return $rpId;
}
return $appId;
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Webauthn\AttestationStatement\AttestationObject;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
*/
class AuthenticatorAttestationResponse extends AuthenticatorResponse
{
/**
* @var AttestationObject
*/
private $attestationObject;
public function __construct(CollectedClientData $clientDataJSON, AttestationObject $attestationObject)
{
parent::__construct($clientDataJSON);
$this->attestationObject = $attestationObject;
}
public function getAttestationObject(): AttestationObject
{
return $this->attestationObject;
}
}

View file

@ -0,0 +1,384 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_string;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Ramsey\Uuid\Uuid;
use function Safe\parse_url;
use function Safe\sprintf;
use Throwable;
use Webauthn\AttestationStatement\AttestationObject;
use Webauthn\AttestationStatement\AttestationStatement;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\CertificateChainChecker\CertificateChainChecker;
use Webauthn\MetadataService\MetadataStatement;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\StatusReport;
use Webauthn\TokenBinding\TokenBindingHandler;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
class AuthenticatorAttestationResponseValidator
{
/**
* @var AttestationStatementSupportManager
*/
private $attestationStatementSupportManager;
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSource;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var MetadataStatementRepository|null
*/
private $metadataStatementRepository;
/**
* @var CertificateChainChecker|null
*/
private $certificateChainChecker;
public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, PublicKeyCredentialSourceRepository $publicKeyCredentialSource, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
{
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
}
if (null !== $metadataStatementRepository) {
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatementRepository".', E_USER_DEPRECATED);
}
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
$this->publicKeyCredentialSource = $publicKeyCredentialSource;
$this->tokenBindingHandler = $tokenBindingHandler;
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
$this->metadataStatementRepository = $metadataStatementRepository;
$this->logger = $logger ?? new NullLogger();
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function setCertificateChainChecker(CertificateChainChecker $certificateChainChecker): self
{
$this->certificateChainChecker = $certificateChainChecker;
return $this;
}
public function setMetadataStatementRepository(MetadataStatementRepository $metadataStatementRepository): self
{
$this->metadataStatementRepository = $metadataStatementRepository;
return $this;
}
/**
* @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
*/
public function check(AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $request, array $securedRelyingPartyId = []): PublicKeyCredentialSource
{
try {
$this->logger->info('Checking the authenticator attestation response', [
'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
'host' => $request->getUri()->getHost(),
]);
/** @see 7.1.1 */
//Nothing to do
/** @see 7.1.2 */
$C = $authenticatorAttestationResponse->getClientDataJSON();
/** @see 7.1.3 */
Assertion::eq('webauthn.create', $C->getType(), 'The client data type is not "webauthn.create".');
/** @see 7.1.4 */
Assertion::true(hash_equals($publicKeyCredentialCreationOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
/** @see 7.1.5 */
$rpId = $publicKeyCredentialCreationOptions->getRp()->getId() ?? $request->getUri()->getHost();
$facetId = $this->getFacetId($rpId, $publicKeyCredentialCreationOptions->getExtensions(), $authenticatorAttestationResponse->getAttestationObject()->getAuthData()->getExtensions());
$parsedRelyingPartyId = parse_url($C->getOrigin());
Assertion::isArray($parsedRelyingPartyId, sprintf('The origin URI "%s" is not valid', $C->getOrigin()));
Assertion::keyExists($parsedRelyingPartyId, 'scheme', 'Invalid origin rpId.');
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
if (!in_array($facetId, $securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
}
/** @see 7.1.6 */
if (null !== $C->getTokenBinding()) {
$this->tokenBindingHandler->check($C->getTokenBinding(), $request);
}
/** @see 7.1.7 */
$clientDataJSONHash = hash('sha256', $authenticatorAttestationResponse->getClientDataJSON()->getRawData(), true);
/** @see 7.1.8 */
$attestationObject = $authenticatorAttestationResponse->getAttestationObject();
/** @see 7.1.9 */
$rpIdHash = hash('sha256', $facetId, true);
Assertion::true(hash_equals($rpIdHash, $attestationObject->getAuthData()->getRpIdHash()), 'rpId hash mismatch.');
/** @see 7.1.10 */
Assertion::true($attestationObject->getAuthData()->isUserPresent(), 'User was not present');
/** @see 7.1.11 */
if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialCreationOptions->getAuthenticatorSelection()->getUserVerification()) {
Assertion::true($attestationObject->getAuthData()->isUserVerified(), 'User authentication required.');
}
/** @see 7.1.12 */
$extensionsClientOutputs = $attestationObject->getAuthData()->getExtensions();
if (null !== $extensionsClientOutputs) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialCreationOptions->getExtensions(),
$extensionsClientOutputs
);
}
/** @see 7.1.13 */
$this->checkMetadataStatement($publicKeyCredentialCreationOptions, $attestationObject);
$fmt = $attestationObject->getAttStmt()->getFmt();
Assertion::true($this->attestationStatementSupportManager->has($fmt), 'Unsupported attestation statement format.');
/** @see 7.1.14 */
$attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
Assertion::true($attestationStatementSupport->isValid($clientDataJSONHash, $attestationObject->getAttStmt(), $attestationObject->getAuthData()), 'Invalid attestation statement.');
/** @see 7.1.15 */
/** @see 7.1.16 */
/** @see 7.1.17 */
Assertion::true($attestationObject->getAuthData()->hasAttestedCredentialData(), 'There is no attested credential data.');
$attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'There is no attested credential data.');
$credentialId = $attestedCredentialData->getCredentialId();
Assertion::null($this->publicKeyCredentialSource->findOneByCredentialId($credentialId), 'The credential ID already exists.');
/** @see 7.1.18 */
/** @see 7.1.19 */
$publicKeyCredentialSource = $this->createPublicKeyCredentialSource(
$credentialId,
$attestedCredentialData,
$attestationObject,
$publicKeyCredentialCreationOptions->getUser()->getId()
);
$this->logger->info('The attestation is valid');
$this->logger->debug('Public Key Credential Source', ['publicKeyCredentialSource' => $publicKeyCredentialSource]);
return $publicKeyCredentialSource;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
private function checkCertificateChain(AttestationStatement $attestationStatement, ?MetadataStatement $metadataStatement): void
{
$trustPath = $attestationStatement->getTrustPath();
if (!$trustPath instanceof CertificateTrustPath) {
return;
}
$authenticatorCertificates = $trustPath->getCertificates();
if (null === $metadataStatement) {
// @phpstan-ignore-next-line
null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, [], null);
return;
}
$metadataStatementCertificates = $metadataStatement->getAttestationRootCertificates();
$rootStatementCertificates = $metadataStatement->getRootCertificates();
foreach ($metadataStatementCertificates as $key => $metadataStatementCertificate) {
$metadataStatementCertificates[$key] = CertificateToolbox::fixPEMStructure($metadataStatementCertificate);
}
$trustedCertificates = array_merge(
$metadataStatementCertificates,
$rootStatementCertificates
);
// @phpstan-ignore-next-line
null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates, $trustedCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, $trustedCertificates);
}
private function checkMetadataStatement(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, AttestationObject $attestationObject): void
{
$attestationStatement = $attestationObject->getAttStmt();
$attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$aaguid = $attestedCredentialData->getAaguid()->toString();
if (PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE === $publicKeyCredentialCreationOptions->getAttestation()) {
$this->logger->debug('No attestation is asked.');
//No attestation is asked. We shall ensure that the data is anonymous.
if (
'00000000-0000-0000-0000-000000000000' === $aaguid
&& (AttestationStatement::TYPE_NONE === $attestationStatement->getType() || AttestationStatement::TYPE_SELF === $attestationStatement->getType())) {
$this->logger->debug('The Attestation Statement is anonymous.');
$this->checkCertificateChain($attestationStatement, null);
return;
}
$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
'aaguid' => $aaguid,
'AttestationStatement' => $attestationStatement,
]);
$attestedCredentialData->setAaguid(
Uuid::fromString('00000000-0000-0000-0000-000000000000')
);
$attestationObject->setAttStmt(AttestationStatement::createNone('none', [], new EmptyTrustPath()));
return;
}
if (AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
$this->logger->debug('No attestation returned.');
//No attestation is returned. We shall ensure that the AAGUID is a null one.
if ('00000000-0000-0000-0000-000000000000' !== $aaguid) {
$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
'aaguid' => $aaguid,
'AttestationStatement' => $attestationStatement,
]);
$attestedCredentialData->setAaguid(
Uuid::fromString('00000000-0000-0000-0000-000000000000')
);
return;
}
return;
}
//The MDS Repository is mandatory here
Assertion::notNull($this->metadataStatementRepository, 'The Metadata Statement Repository is mandatory when requesting attestation objects.');
$metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
// We check the last status report
$this->checkStatusReport(null === $metadataStatement ? [] : $metadataStatement->getStatusReports());
// We check the certificate chain (if any)
$this->checkCertificateChain($attestationStatement, $metadataStatement);
// If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
// => nothing to check
if ('00000000-0000-0000-0000-000000000000' === $aaguid || AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
return;
}
// At this point, the Metadata Statement is mandatory
Assertion::notNull($metadataStatement, sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid));
// Check Attestation Type is allowed
if (0 !== count($metadataStatement->getAttestationTypes())) {
$type = $this->getAttestationType($attestationStatement);
Assertion::inArray($type, $metadataStatement->getAttestationTypes(), 'Invalid attestation statement. The attestation type is not allowed for this authenticator');
}
}
/**
* @param StatusReport[] $statusReports
*/
private function checkStatusReport(array $statusReports): void
{
if (0 !== count($statusReports)) {
$lastStatusReport = end($statusReports);
if ($lastStatusReport->isCompromised()) {
throw new LogicException('The authenticator is compromised and cannot be used');
}
}
}
private function createPublicKeyCredentialSource(string $credentialId, AttestedCredentialData $attestedCredentialData, AttestationObject $attestationObject, string $userHandle): PublicKeyCredentialSource
{
return new PublicKeyCredentialSource(
$credentialId,
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
[],
$attestationObject->getAttStmt()->getType(),
$attestationObject->getAttStmt()->getTrustPath(),
$attestedCredentialData->getAaguid(),
$attestedCredentialData->getCredentialPublicKey(),
$userHandle,
$attestationObject->getAuthData()->getSignCount()
);
}
private function getAttestationType(AttestationStatement $attestationStatement): int
{
switch ($attestationStatement->getType()) {
case AttestationStatement::TYPE_BASIC:
return MetadataStatement::ATTESTATION_BASIC_FULL;
case AttestationStatement::TYPE_SELF:
return MetadataStatement::ATTESTATION_BASIC_SURROGATE;
case AttestationStatement::TYPE_ATTCA:
return MetadataStatement::ATTESTATION_ATTCA;
case AttestationStatement::TYPE_ECDAA:
return MetadataStatement::ATTESTATION_ECDAA;
default:
throw new InvalidArgumentException('Invalid attestation type');
}
}
private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
{
if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')->value();
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
if (!is_string($appId) || true !== $wasUsed) {
return $rpId;
}
return $appId;
}
}

View file

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function ord;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
/**
* @see https://www.w3.org/TR/webauthn/#sec-authenticator-data
*/
class AuthenticatorData
{
private const FLAG_UP = 0b00000001;
private const FLAG_RFU1 = 0b00000010;
private const FLAG_UV = 0b00000100;
private const FLAG_RFU2 = 0b00111000;
private const FLAG_AT = 0b01000000;
private const FLAG_ED = 0b10000000;
/**
* @var string
*/
protected $authData;
/**
* @var string
*/
protected $rpIdHash;
/**
* @var string
*/
protected $flags;
/**
* @var int
*/
protected $signCount;
/**
* @var AttestedCredentialData|null
*/
protected $attestedCredentialData;
/**
* @var AuthenticationExtensionsClientOutputs|null
*/
protected $extensions;
public function __construct(string $authData, string $rpIdHash, string $flags, int $signCount, ?AttestedCredentialData $attestedCredentialData, ?AuthenticationExtensionsClientOutputs $extensions)
{
$this->rpIdHash = $rpIdHash;
$this->flags = $flags;
$this->signCount = $signCount;
$this->attestedCredentialData = $attestedCredentialData;
$this->extensions = $extensions;
$this->authData = $authData;
}
public function getAuthData(): string
{
return $this->authData;
}
public function getRpIdHash(): string
{
return $this->rpIdHash;
}
public function isUserPresent(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_UP) ? true : false;
}
public function isUserVerified(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_UV) ? true : false;
}
public function hasAttestedCredentialData(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_AT) ? true : false;
}
public function hasExtensions(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_ED) ? true : false;
}
public function getReservedForFutureUse1(): int
{
return ord($this->flags) & self::FLAG_RFU1;
}
public function getReservedForFutureUse2(): int
{
return ord($this->flags) & self::FLAG_RFU2;
}
public function getSignCount(): int
{
return $this->signCount;
}
public function getAttestedCredentialData(): ?AttestedCredentialData
{
return $this->attestedCredentialData;
}
public function getExtensions(): ?AuthenticationExtensionsClientOutputs
{
return null !== $this->extensions && $this->hasExtensions() ? $this->extensions : null;
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorresponse
*/
abstract class AuthenticatorResponse
{
/**
* @var CollectedClientData
*/
private $clientDataJSON;
public function __construct(CollectedClientData $clientDataJSON)
{
$this->clientDataJSON = $clientDataJSON;
}
public function getClientDataJSON(): CollectedClientData
{
return $this->clientDataJSON;
}
}

View file

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use JsonSerializable;
use function Safe\json_decode;
class AuthenticatorSelectionCriteria implements JsonSerializable
{
public const AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE = null;
public const AUTHENTICATOR_ATTACHMENT_PLATFORM = 'platform';
public const AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 'cross-platform';
public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
public const RESIDENT_KEY_REQUIREMENT_NONE = null;
public const RESIDENT_KEY_REQUIREMENT_REQUIRED = 'required';
public const RESIDENT_KEY_REQUIREMENT_PREFERRED = 'preferred';
public const RESIDENT_KEY_REQUIREMENT_DISCOURAGED = 'discouraged';
/**
* @var string|null
*/
private $authenticatorAttachment;
/**
* @var bool
*/
private $requireResidentKey;
/**
* @var string
*/
private $userVerification;
/**
* @var string|null
*/
private $residentKey;
public function __construct(?string $authenticatorAttachment = null, ?bool $requireResidentKey = null, ?string $userVerification = null, ?string $residentKey = null)
{
if (null !== $authenticatorAttachment) {
@trigger_error('The argument "authenticatorAttachment" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setAuthenticatorAttachment".', E_USER_DEPRECATED);
}
if (null !== $requireResidentKey) {
@trigger_error('The argument "requireResidentKey" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setRequireResidentKey".', E_USER_DEPRECATED);
}
if (null !== $userVerification) {
@trigger_error('The argument "userVerification" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setUserVerification".', E_USER_DEPRECATED);
}
if (null !== $residentKey) {
@trigger_error('The argument "residentKey" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setResidentKey".', E_USER_DEPRECATED);
}
$this->authenticatorAttachment = $authenticatorAttachment;
$this->requireResidentKey = $requireResidentKey ?? false;
$this->userVerification = $userVerification ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED;
$this->residentKey = $residentKey ?? self::RESIDENT_KEY_REQUIREMENT_NONE;
}
public static function create(): self
{
return new self();
}
public function setAuthenticatorAttachment(?string $authenticatorAttachment): self
{
$this->authenticatorAttachment = $authenticatorAttachment;
return $this;
}
public function setRequireResidentKey(bool $requireResidentKey): self
{
$this->requireResidentKey = $requireResidentKey;
return $this;
}
public function setUserVerification(string $userVerification): self
{
$this->userVerification = $userVerification;
return $this;
}
public function setResidentKey(?string $residentKey): self
{
$this->residentKey = $residentKey;
return $this;
}
public function getAuthenticatorAttachment(): ?string
{
return $this->authenticatorAttachment;
}
public function isRequireResidentKey(): bool
{
return $this->requireResidentKey;
}
public function getUserVerification(): string
{
return $this->userVerification;
}
public function getResidentKey(): ?string
{
return $this->residentKey;
}
public static function createFromString(string $data): self
{
$data = json_decode($data, true);
Assertion::isArray($data, 'Invalid data');
return self::createFromArray($data);
}
/**
* @param mixed[] $json
*/
public static function createFromArray(array $json): self
{
return self::create()
->setAuthenticatorAttachment($json['authenticatorAttachment'] ?? null)
->setRequireResidentKey($json['requireResidentKey'] ?? false)
->setUserVerification($json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED)
->setResidentKey($json['residentKey'] ?? self::RESIDENT_KEY_REQUIREMENT_NONE)
;
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
$json = [
'requireResidentKey' => $this->requireResidentKey,
'userVerification' => $this->userVerification,
];
if (null !== $this->authenticatorAttachment) {
$json['authenticatorAttachment'] = $this->authenticatorAttachment;
}
if (null !== $this->residentKey) {
$json['residentKey'] = $this->residentKey;
}
return $json;
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\CertificateChainChecker;
interface CertificateChainChecker
{
/**
* @param string[] $authenticatorCertificates
* @param string[] $trustedCertificates
*/
public function check(array $authenticatorCertificates, array $trustedCertificates): void;
}

View file

@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\CertificateChainChecker;
use Assert\Assertion;
use function count;
use InvalidArgumentException;
use function is_int;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
use Safe\Exceptions\FilesystemException;
use function Safe\file_put_contents;
use function Safe\ksort;
use function Safe\mkdir;
use function Safe\rename;
use function Safe\sprintf;
use function Safe\tempnam;
use function Safe\unlink;
use Symfony\Component\Process\Process;
final class OpenSSLCertificateChainChecker implements CertificateChainChecker
{
/**
* @var ClientInterface
*/
private $client;
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var string[]
*/
private $rootCertificates = [];
public function __construct(ClientInterface $client, RequestFactoryInterface $requestFactory)
{
$this->client = $client;
$this->requestFactory = $requestFactory;
}
public function addRootCertificate(string $certificate): self
{
$this->rootCertificates[] = $certificate;
return $this;
}
/**
* @param string[] $authenticatorCertificates
* @param string[] $trustedCertificates
*/
public function check(array $authenticatorCertificates, array $trustedCertificates): void
{
if (0 === count($trustedCertificates)) {
$this->checkCertificatesValidity($authenticatorCertificates, true);
return;
}
$this->checkCertificatesValidity($authenticatorCertificates, false);
$hasCrls = false;
$processArguments = ['-no-CAfile', '-no-CApath'];
$caDirname = $this->createTemporaryDirectory();
$processArguments[] = '--CApath';
$processArguments[] = $caDirname;
foreach ($trustedCertificates as $certificate) {
$this->saveToTemporaryFile($caDirname, $certificate, 'webauthn-trusted-', '.pem');
$crl = $this->getCrls($certificate);
if ('' !== $crl) {
$hasCrls = true;
$this->saveToTemporaryFile($caDirname, $crl, 'webauthn-trusted-crl-', '.crl');
}
}
$rehashProcess = new Process(['openssl', 'rehash', $caDirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
if (!$rehashProcess->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
$filenames = [];
$leafCertificate = array_shift($authenticatorCertificates);
$leafFilename = $this->saveToTemporaryFile(sys_get_temp_dir(), $leafCertificate, 'webauthn-leaf-', '.pem');
$crl = $this->getCrls($leafCertificate);
if ('' !== $crl) {
$hasCrls = true;
$this->saveToTemporaryFile($caDirname, $crl, 'webauthn-leaf-crl-', '.pem');
}
$filenames[] = $leafFilename;
foreach ($authenticatorCertificates as $certificate) {
$untrustedFilename = $this->saveToTemporaryFile(sys_get_temp_dir(), $certificate, 'webauthn-untrusted-', '.pem');
$crl = $this->getCrls($certificate);
if ('' !== $crl) {
$hasCrls = true;
$this->saveToTemporaryFile($caDirname, $crl, 'webauthn-untrusted-crl-', '.pem');
}
$processArguments[] = '-untrusted';
$processArguments[] = $untrustedFilename;
$filenames[] = $untrustedFilename;
}
$processArguments[] = $leafFilename;
if ($hasCrls) {
array_unshift($processArguments, '-crl_check');
array_unshift($processArguments, '-crl_check_all');
//array_unshift($processArguments, '-crl_download');
array_unshift($processArguments, '-extended_crl');
}
array_unshift($processArguments, 'openssl', 'verify');
$process = new Process($processArguments);
$process->run();
while ($process->isRunning()) {
//Just wait
}
foreach ($filenames as $filename) {
try {
unlink($filename);
} catch (FilesystemException $e) {
continue;
}
}
$this->deleteDirectory($caDirname);
if (!$process->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
}
/**
* @param string[] $certificates
*/
private function checkCertificatesValidity(array $certificates, bool $allowRootCertificate): void
{
foreach ($certificates as $certificate) {
$parsed = openssl_x509_parse($certificate);
Assertion::isArray($parsed, 'Unable to read the certificate');
if (false === $allowRootCertificate) {
$this->checkRootCertificate($parsed);
}
Assertion::keyExists($parsed, 'validTo_time_t', 'The certificate has no validity period');
Assertion::keyExists($parsed, 'validFrom_time_t', 'The certificate has no validity period');
Assertion::lessOrEqualThan(time(), $parsed['validTo_time_t'], 'The certificate expired');
Assertion::greaterOrEqualThan(time(), $parsed['validFrom_time_t'], 'The certificate is not usable yet');
}
}
/**
* @param array<string, mixed> $parsed
*/
private function checkRootCertificate(array $parsed): void
{
Assertion::keyExists($parsed, 'subject', 'The certificate has no subject');
Assertion::keyExists($parsed, 'issuer', 'The certificate has no issuer');
$subject = $parsed['subject'];
$issuer = $parsed['issuer'];
ksort($subject);
ksort($issuer);
Assertion::notEq($subject, $issuer, 'Root certificates are not allowed');
}
private function createTemporaryDirectory(): string
{
$caDir = tempnam(sys_get_temp_dir(), 'webauthn-ca-');
if (file_exists($caDir)) {
unlink($caDir);
}
mkdir($caDir);
if (!is_dir($caDir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $caDir));
}
return $caDir;
}
private function deleteDirectory(string $dirname): void
{
$rehashProcess = new Process(['rm', '-rf', $dirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
}
private function saveToTemporaryFile(string $folder, string $certificate, string $prefix, string $suffix): string
{
$filename = tempnam($folder, $prefix);
rename($filename, $filename.$suffix);
file_put_contents($filename.$suffix, $certificate, FILE_APPEND);
return $filename.$suffix;
}
private function getCrls(string $certificate): string
{
$parsed = openssl_x509_parse($certificate);
if (false === $parsed || !isset($parsed['extensions']['crlDistributionPoints'])) {
return '';
}
$endpoint = $parsed['extensions']['crlDistributionPoints'];
$pos = mb_strpos($endpoint, 'URI:');
if (!is_int($pos)) {
return '';
}
$endpoint = trim(mb_substr($endpoint, $pos + 4));
$request = $this->requestFactory->createRequest('GET', $endpoint);
$response = $this->client->sendRequest($request);
if (200 !== $response->getStatusCode()) {
return '';
}
return $response->getBody()->getContents();
}
}

View file

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use function count;
use function in_array;
use InvalidArgumentException;
use RuntimeException;
use Safe\Exceptions\FilesystemException;
use function Safe\file_put_contents;
use function Safe\ksort;
use function Safe\mkdir;
use function Safe\rename;
use function Safe\sprintf;
use function Safe\tempnam;
use function Safe\unlink;
use Symfony\Component\Process\Process;
class CertificateToolbox
{
/**
* @deprecated "This method is deprecated since v3.3 and will be removed en v4.0. Please use Webauthn\CertificateChainChecker\CertificateChainChecker instead"
*
* @param string[] $authenticatorCertificates
* @param string[] $trustedCertificates
*/
public static function checkChain(array $authenticatorCertificates, array $trustedCertificates = []): void
{
if (0 === count($trustedCertificates)) {
self::checkCertificatesValidity($authenticatorCertificates, true);
return;
}
self::checkCertificatesValidity($authenticatorCertificates, false);
$processArguments = ['-no-CAfile', '-no-CApath'];
$caDirname = self::createTemporaryDirectory();
$processArguments[] = '--CApath';
$processArguments[] = $caDirname;
foreach ($trustedCertificates as $certificate) {
self::prepareCertificate($caDirname, $certificate, 'webauthn-trusted-', '.pem');
}
$rehashProcess = new Process(['openssl', 'rehash', $caDirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
if (!$rehashProcess->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
$filenames = [];
$leafCertificate = array_shift($authenticatorCertificates);
$leafFilename = self::prepareCertificate(sys_get_temp_dir(), $leafCertificate, 'webauthn-leaf-', '.pem');
$filenames[] = $leafFilename;
foreach ($authenticatorCertificates as $certificate) {
$untrustedFilename = self::prepareCertificate(sys_get_temp_dir(), $certificate, 'webauthn-untrusted-', '.pem');
$processArguments[] = '-untrusted';
$processArguments[] = $untrustedFilename;
$filenames[] = $untrustedFilename;
}
$processArguments[] = $leafFilename;
array_unshift($processArguments, 'openssl', 'verify');
$process = new Process($processArguments);
$process->run();
while ($process->isRunning()) {
//Just wait
}
foreach ($filenames as $filename) {
try {
unlink($filename);
} catch (FilesystemException $e) {
continue;
}
}
self::deleteDirectory($caDirname);
if (!$process->isSuccessful()) {
throw new InvalidArgumentException('Invalid certificate or certificate chain');
}
}
public static function fixPEMStructure(string $certificate, string $type = 'CERTIFICATE'): string
{
$pemCert = '-----BEGIN '.$type.'-----'.PHP_EOL;
$pemCert .= chunk_split($certificate, 64, PHP_EOL);
$pemCert .= '-----END '.$type.'-----'.PHP_EOL;
return $pemCert;
}
public static function convertDERToPEM(string $certificate, string $type = 'CERTIFICATE'): string
{
$derCertificate = self::unusedBytesFix($certificate);
return self::fixPEMStructure(base64_encode($derCertificate), $type);
}
/**
* @param string[] $certificates
*
* @return string[]
*/
public static function convertAllDERToPEM(array $certificates, string $type = 'CERTIFICATE'): array
{
$certs = [];
foreach ($certificates as $publicKey) {
$certs[] = self::convertDERToPEM($publicKey, $type);
}
return $certs;
}
private static function unusedBytesFix(string $certificate): string
{
$certificateHash = hash('sha256', $certificate);
if (in_array($certificateHash, self::getCertificateHashes(), true)) {
$certificate[mb_strlen($certificate, '8bit') - 257] = "\0";
}
return $certificate;
}
/**
* @param string[] $certificates
*/
private static function checkCertificatesValidity(array $certificates, bool $allowRootCertificate): void
{
foreach ($certificates as $certificate) {
$parsed = openssl_x509_parse($certificate);
Assertion::isArray($parsed, 'Unable to read the certificate');
if (false === $allowRootCertificate) {
self::checkRootCertificate($parsed);
}
Assertion::keyExists($parsed, 'validTo_time_t', 'The certificate has no validity period');
Assertion::keyExists($parsed, 'validFrom_time_t', 'The certificate has no validity period');
Assertion::lessOrEqualThan(time(), $parsed['validTo_time_t'], 'The certificate expired');
Assertion::greaterOrEqualThan(time(), $parsed['validFrom_time_t'], 'The certificate is not usable yet');
}
}
/**
* @param array<string, mixed> $parsed
*/
private static function checkRootCertificate(array $parsed): void
{
Assertion::keyExists($parsed, 'subject', 'The certificate has no subject');
Assertion::keyExists($parsed, 'issuer', 'The certificate has no issuer');
$subject = $parsed['subject'];
$issuer = $parsed['issuer'];
ksort($subject);
ksort($issuer);
Assertion::notEq($subject, $issuer, 'Root certificates are not allowed');
}
/**
* @return string[]
*/
private static function getCertificateHashes(): array
{
return [
'349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
'1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
'6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511',
];
}
private static function createTemporaryDirectory(): string
{
$caDir = tempnam(sys_get_temp_dir(), 'webauthn-ca-');
if (file_exists($caDir)) {
unlink($caDir);
}
mkdir($caDir);
if (!is_dir($caDir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $caDir));
}
return $caDir;
}
private static function deleteDirectory(string $dirname): void
{
$rehashProcess = new Process(['rm', '-rf', $dirname]);
$rehashProcess->run();
while ($rehashProcess->isRunning()) {
//Just wait
}
}
private static function prepareCertificate(string $folder, string $certificate, string $prefix, string $suffix): string
{
$untrustedFilename = tempnam($folder, $prefix);
rename($untrustedFilename, $untrustedFilename.$suffix);
file_put_contents($untrustedFilename.$suffix, $certificate, FILE_APPEND);
file_put_contents($untrustedFilename.$suffix, PHP_EOL, FILE_APPEND);
return $untrustedFilename.$suffix;
}
}

View file

@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use function array_key_exists;
use Assert\Assertion;
use Base64Url\Base64Url;
use InvalidArgumentException;
use function Safe\json_decode;
use function Safe\sprintf;
use Webauthn\TokenBinding\TokenBinding;
class CollectedClientData
{
/**
* @var string
*/
private $rawData;
/**
* @var mixed[]
*/
private $data;
/**
* @var string
*/
private $type;
/**
* @var string
*/
private $challenge;
/**
* @var string
*/
private $origin;
/**
* @var mixed[]|null
*/
private $tokenBinding;
/**
* @param mixed[] $data
*/
public function __construct(string $rawData, array $data)
{
$this->type = $this->findData($data, 'type');
$this->challenge = $this->findData($data, 'challenge', true, true);
$this->origin = $this->findData($data, 'origin');
$this->tokenBinding = $this->findData($data, 'tokenBinding', false);
$this->rawData = $rawData;
$this->data = $data;
}
public static function createFormJson(string $data): self
{
$rawData = Base64Url::decode($data);
$json = json_decode($rawData, true);
Assertion::isArray($json, 'Invalid collected client data');
return new self($rawData, $json);
}
public function getType(): string
{
return $this->type;
}
public function getChallenge(): string
{
return $this->challenge;
}
public function getOrigin(): string
{
return $this->origin;
}
public function getTokenBinding(): ?TokenBinding
{
return null === $this->tokenBinding ? null : TokenBinding::createFormArray($this->tokenBinding);
}
public function getRawData(): string
{
return $this->rawData;
}
/**
* @return string[]
*/
public function all(): array
{
return array_keys($this->data);
}
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
/**
* @return mixed
*/
public function get(string $key)
{
if (!$this->has($key)) {
throw new InvalidArgumentException(sprintf('The key "%s" is missing', $key));
}
return $this->data[$key];
}
/**
* @param mixed[] $json
*
* @return mixed|null
*/
private function findData(array $json, string $key, bool $isRequired = true, bool $isB64 = false)
{
if (!array_key_exists($key, $json)) {
if ($isRequired) {
throw new InvalidArgumentException(sprintf('The key "%s" is missing', $key));
}
return;
}
return $isB64 ? Base64Url::decode($json[$key]) : $json[$key];
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn\Counter;
use Webauthn\PublicKeyCredentialSource;
interface CounterChecker
{
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void;
}

Some files were not shown because too many files have changed in this diff Show more