Update website
This commit is contained in:
parent
bb4b0f9be8
commit
011b183e28
4263 changed files with 3014 additions and 720369 deletions
|
@ -1,21 +0,0 @@
|
|||
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.
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"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/"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
<?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];
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
<?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];
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
<?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));
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
<?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];
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
<?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));
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<?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
|
||||
{
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
<?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; });
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,283 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,602 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,166 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?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; });
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
<?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 don’t 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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,292 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<?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];
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,309 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,384 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,223 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
<?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];
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<?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
Loading…
Add table
Add a link
Reference in a new issue