Обновление клиента (apps, 3rdparty, install)
This commit is contained in:
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Spomky-Labs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm;
|
||||
|
||||
interface Algorithm
|
||||
{
|
||||
public static function identifier(): int;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Mac;
|
||||
|
||||
final class HS256 extends Hmac
|
||||
{
|
||||
public const ID = 5;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): string
|
||||
{
|
||||
return 'sha256';
|
||||
}
|
||||
|
||||
protected function getSignatureLength(): int
|
||||
{
|
||||
return 256;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Mac;
|
||||
|
||||
final class HS256Truncated64 extends Hmac
|
||||
{
|
||||
public const ID = 4;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): string
|
||||
{
|
||||
return 'sha256';
|
||||
}
|
||||
|
||||
protected function getSignatureLength(): int
|
||||
{
|
||||
return 64;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Mac;
|
||||
|
||||
final class HS384 extends Hmac
|
||||
{
|
||||
public const ID = 6;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): string
|
||||
{
|
||||
return 'sha384';
|
||||
}
|
||||
|
||||
protected function getSignatureLength(): int
|
||||
{
|
||||
return 384;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Mac;
|
||||
|
||||
final class HS512 extends Hmac
|
||||
{
|
||||
public const ID = 7;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): string
|
||||
{
|
||||
return 'sha512';
|
||||
}
|
||||
|
||||
protected function getSignatureLength(): int
|
||||
{
|
||||
return 512;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Mac;
|
||||
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\SymmetricKey;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @see \Cose\Tests\Algorithm\Mac\HmacTest
|
||||
*/
|
||||
abstract class Hmac implements Mac
|
||||
{
|
||||
public function hash(string $data, Key $key): string
|
||||
{
|
||||
$this->checKey($key);
|
||||
$signature = hash_hmac($this->getHashAlgorithm(), $data, (string) $key->get(SymmetricKey::DATA_K), 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
|
||||
{
|
||||
if ($key->type() !== Key::TYPE_OCT && $key->type() !== Key::TYPE_NAME_OCT) {
|
||||
throw new InvalidArgumentException('Invalid key. Must be of type symmetric');
|
||||
}
|
||||
|
||||
if (! $key->has(SymmetricKey::DATA_K)) {
|
||||
throw new InvalidArgumentException('Invalid key. The value of the key is missing');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function array_key_exists;
|
||||
|
||||
final class Manager
|
||||
{
|
||||
/**
|
||||
* @var array<int, Algorithm>
|
||||
*/
|
||||
private array $algorithms = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function add(Algorithm ...$algorithms): self
|
||||
{
|
||||
foreach ($algorithms as $algorithm) {
|
||||
$identifier = $algorithm::identifier();
|
||||
$this->algorithms[$identifier] = $algorithm;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<int>
|
||||
*/
|
||||
public function list(): iterable
|
||||
{
|
||||
yield from array_keys($this->algorithms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<int, 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
|
||||
{
|
||||
if (! $this->has($identifier)) {
|
||||
throw new InvalidArgumentException('Unsupported algorithm');
|
||||
}
|
||||
|
||||
return $this->algorithms[$identifier];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function array_key_exists;
|
||||
|
||||
final class ManagerFactory
|
||||
{
|
||||
/**
|
||||
* @var array<string, Algorithm>
|
||||
*/
|
||||
private array $algorithms = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function add(string $alias, Algorithm $algorithm): self
|
||||
{
|
||||
$this->algorithms[$alias] = $algorithm;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function list(): iterable
|
||||
{
|
||||
yield from array_keys($this->algorithms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Algorithm[]
|
||||
*/
|
||||
public function all(): iterable
|
||||
{
|
||||
yield from $this->algorithms;
|
||||
}
|
||||
|
||||
public function generate(string ...$aliases): Manager
|
||||
{
|
||||
$manager = Manager::create();
|
||||
foreach ($aliases as $alias) {
|
||||
if (! array_key_exists($alias, $this->algorithms)) {
|
||||
throw new InvalidArgumentException(sprintf('The algorithm with alias "%s" is not supported', $alias));
|
||||
}
|
||||
$manager->add($this->algorithms[$alias]);
|
||||
}
|
||||
|
||||
return $manager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\ECDSA;
|
||||
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use InvalidArgumentException;
|
||||
use function openssl_sign;
|
||||
use function openssl_verify;
|
||||
|
||||
/**
|
||||
* @see \Cose\Tests\Algorithm\Signature\ECDSA\ECDSATest
|
||||
*/
|
||||
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 openssl_verify($data, $signature, $publicKey->asPEM(), $this->getHashAlgorithm()) === 1;
|
||||
}
|
||||
|
||||
abstract protected function getCurve(): int;
|
||||
|
||||
abstract protected function getHashAlgorithm(): int;
|
||||
|
||||
abstract protected function getSignaturePartLength(): int;
|
||||
|
||||
private function handleKey(Key $key): Ec2Key
|
||||
{
|
||||
$key = Ec2Key::create($key->getData());
|
||||
if ($key->curve() !== $this->getCurve()) {
|
||||
throw new InvalidArgumentException('This key cannot be used with this algorithm');
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\ECDSA;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function bin2hex;
|
||||
use function dechex;
|
||||
use function hex2bin;
|
||||
use function hexdec;
|
||||
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 : '';
|
||||
|
||||
return hex2bin(
|
||||
self::ASN1_SEQUENCE
|
||||
. $lengthPrefix . dechex($totalLength)
|
||||
. self::ASN1_INTEGER . dechex($lengthR) . $pointR
|
||||
. self::ASN1_INTEGER . dechex($lengthS) . $pointS
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromAsn1(string $signature, int $length): string
|
||||
{
|
||||
$message = bin2hex($signature);
|
||||
$position = 0;
|
||||
|
||||
if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) {
|
||||
throw new InvalidArgumentException('Invalid data. Should start with a sequence.');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) {
|
||||
$position += self::BYTE_SIZE;
|
||||
}
|
||||
|
||||
$pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
|
||||
$pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
|
||||
|
||||
return hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT) . str_pad($pointS, $length, '0', STR_PAD_LEFT));
|
||||
}
|
||||
|
||||
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 (
|
||||
mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0
|
||||
&& 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::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) {
|
||||
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 (
|
||||
mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0
|
||||
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT
|
||||
) {
|
||||
$data = mb_substr($data, 2, null, '8bit');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\ECDSA;
|
||||
|
||||
use Cose\Key\Ec2Key;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
|
||||
final class ES256 extends ECDSA
|
||||
{
|
||||
public const ID = -7;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\ECDSA;
|
||||
|
||||
use Cose\Key\Ec2Key;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
|
||||
final class ES256K extends ECDSA
|
||||
{
|
||||
public const ID = -46;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\ECDSA;
|
||||
|
||||
use Cose\Key\Ec2Key;
|
||||
use const OPENSSL_ALGO_SHA384;
|
||||
|
||||
final class ES384 extends ECDSA
|
||||
{
|
||||
public const ID = -35;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\ECDSA;
|
||||
|
||||
use Cose\Key\Ec2Key;
|
||||
use const OPENSSL_ALGO_SHA512;
|
||||
|
||||
final class ES512 extends ECDSA
|
||||
{
|
||||
public const ID = -36;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\EdDSA;
|
||||
|
||||
final class Ed25519 extends EdDSA
|
||||
{
|
||||
public const ID = -8;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\EdDSA;
|
||||
|
||||
use Cose\Key\Key;
|
||||
|
||||
final class Ed256 extends EdDSA
|
||||
{
|
||||
public const ID = -260;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\EdDSA;
|
||||
|
||||
use Cose\Key\Key;
|
||||
|
||||
final class Ed512 extends EdDSA
|
||||
{
|
||||
public const ID = -261;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\EdDSA;
|
||||
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\OkpKey;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
use function sodium_crypto_sign_detached;
|
||||
use function sodium_crypto_sign_verify_detached;
|
||||
|
||||
/**
|
||||
* @see \Cose\Tests\Algorithm\Signature\EdDSA\EdDSATest
|
||||
*/
|
||||
class EdDSA implements Signature
|
||||
{
|
||||
public function sign(string $data, Key $key): string
|
||||
{
|
||||
$key = $this->handleKey($key);
|
||||
if (! $key->isPrivate()) {
|
||||
throw new InvalidArgumentException('The key is not private.');
|
||||
}
|
||||
|
||||
$x = $key->x();
|
||||
$d = $key->d();
|
||||
$secret = $d . $x;
|
||||
|
||||
return match ($key->curve()) {
|
||||
OkpKey::CURVE_ED25519 => sodium_crypto_sign_detached($data, $secret),
|
||||
OkpKey::CURVE_NAME_ED25519 => 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);
|
||||
if ($key->curve() !== OkpKey::CURVE_ED25519 && $key->curve() !== OkpKey::CURVE_NAME_ED25519) {
|
||||
throw new InvalidArgumentException('Unsupported curve');
|
||||
}
|
||||
try {
|
||||
sodium_crypto_sign_verify_detached($signature, $data, $key->x());
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return Algorithms::COSE_ALGORITHM_EDDSA;
|
||||
}
|
||||
|
||||
private function handleKey(Key $key): OkpKey
|
||||
{
|
||||
return OkpKey::create($key->getData());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use Cose\Hash;
|
||||
|
||||
final class PS256 extends PSSRSA
|
||||
{
|
||||
public const ID = -37;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): Hash
|
||||
{
|
||||
return Hash::sha256();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use Cose\Hash;
|
||||
|
||||
final class PS384 extends PSSRSA
|
||||
{
|
||||
public const ID = -38;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): Hash
|
||||
{
|
||||
return Hash::sha384();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use Cose\Hash;
|
||||
|
||||
final class PS512 extends PSSRSA
|
||||
{
|
||||
public const ID = -39;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): Hash
|
||||
{
|
||||
return Hash::sha512();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\BigInteger;
|
||||
use Cose\Hash;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use function ceil;
|
||||
use function chr;
|
||||
use function hash_equals;
|
||||
use function mb_strlen;
|
||||
use function mb_substr;
|
||||
use function ord;
|
||||
use function pack;
|
||||
use function random_bytes;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* @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())
|
||||
);
|
||||
}
|
||||
|
||||
[$pS, $qS] = $key->primes();
|
||||
[$dPS, $dQS] = $key->exponents();
|
||||
$qInv = BigInteger::createFromBinaryString($key->QInv());
|
||||
$p = BigInteger::createFromBinaryString($pS);
|
||||
$q = BigInteger::createFromBinaryString($qS);
|
||||
$dP = BigInteger::createFromBinaryString($dPS);
|
||||
$dQ = BigInteger::createFromBinaryString($dQS);
|
||||
|
||||
$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 RsaKey::create($key->getData());
|
||||
}
|
||||
|
||||
private function convertIntegerToOctetString(BigInteger $x, int $xLen): string
|
||||
{
|
||||
$xB = $x->toBytes();
|
||||
if (mb_strlen($xB, '8bit') > $xLen) {
|
||||
throw new RuntimeException('Unable to convert the integer');
|
||||
}
|
||||
|
||||
return str_pad($xB, $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_strpos($db, str_repeat(chr(0), $temp), 0, '8bit') !== 0) {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
if (ord($db[$temp]) !== 1) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use const OPENSSL_ALGO_SHA1;
|
||||
|
||||
final class RS1 extends RSA
|
||||
{
|
||||
public const ID = -65535;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): int
|
||||
{
|
||||
return OPENSSL_ALGO_SHA1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
|
||||
final class RS256 extends RSA
|
||||
{
|
||||
public const ID = -257;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): int
|
||||
{
|
||||
return OPENSSL_ALGO_SHA256;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use const OPENSSL_ALGO_SHA384;
|
||||
|
||||
final class RS384 extends RSA
|
||||
{
|
||||
public const ID = -258;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): int
|
||||
{
|
||||
return OPENSSL_ALGO_SHA384;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use const OPENSSL_ALGO_SHA512;
|
||||
|
||||
final class RS512 extends RSA
|
||||
{
|
||||
public const ID = -259;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public static function identifier(): int
|
||||
{
|
||||
return self::ID;
|
||||
}
|
||||
|
||||
protected function getHashAlgorithm(): int
|
||||
{
|
||||
return OPENSSL_ALGO_SHA512;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Algorithm\Signature\RSA;
|
||||
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
use function openssl_sign;
|
||||
use function openssl_verify;
|
||||
|
||||
/**
|
||||
* @see \Cose\Tests\Algorithm\Signature\RSA\RSATest
|
||||
*/
|
||||
abstract class RSA implements Signature
|
||||
{
|
||||
public function sign(string $data, Key $key): string
|
||||
{
|
||||
$key = $this->handleKey($key);
|
||||
if (! $key->isPrivate()) {
|
||||
throw new InvalidArgumentException('The key is not private.');
|
||||
}
|
||||
|
||||
try {
|
||||
openssl_sign($data, $signature, $key->asPem(), $this->getHashAlgorithm());
|
||||
} catch (Throwable $e) {
|
||||
throw new InvalidArgumentException('Unable to sign the data', 0, $e);
|
||||
}
|
||||
|
||||
return $signature;
|
||||
}
|
||||
|
||||
public function verify(string $data, Key $key, string $signature): bool
|
||||
{
|
||||
$key = $this->handleKey($key);
|
||||
|
||||
return openssl_verify($data, $signature, $key->toPublic()->asPem(), $this->getHashAlgorithm()) === 1;
|
||||
}
|
||||
|
||||
abstract protected function getHashAlgorithm(): int;
|
||||
|
||||
private function handleKey(Key $key): RsaKey
|
||||
{
|
||||
return RsaKey::create($key->getData());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function array_key_exists;
|
||||
use const OPENSSL_ALGO_SHA1;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
use const OPENSSL_ALGO_SHA384;
|
||||
use const OPENSSL_ALGO_SHA512;
|
||||
|
||||
/**
|
||||
* @see https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||
*/
|
||||
abstract class Algorithms
|
||||
{
|
||||
final public const COSE_ALGORITHM_AES_CCM_64_128_256 = 33;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_64_128_128 = 32;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_16_128_256 = 31;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_16_128_128 = 30;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_MAC_256_128 = 26;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_MAC_128_128 = 25;
|
||||
|
||||
final public const COSE_ALGORITHM_CHACHA20_POLY1305 = 24;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_MAC_256_64 = 15;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_MAC_128_64 = 14;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_64_64_256 = 13;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_64_64_128 = 12;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_16_64_256 = 11;
|
||||
|
||||
final public const COSE_ALGORITHM_AES_CCM_16_64_128 = 10;
|
||||
|
||||
final public const COSE_ALGORITHM_HS512 = 7;
|
||||
|
||||
final public const COSE_ALGORITHM_HS384 = 6;
|
||||
|
||||
final public const COSE_ALGORITHM_HS256 = 5;
|
||||
|
||||
final public const COSE_ALGORITHM_HS256_64 = 4;
|
||||
|
||||
final public const COSE_ALGORITHM_A256GCM = 3;
|
||||
|
||||
final public const COSE_ALGORITHM_A192GCM = 2;
|
||||
|
||||
final public const COSE_ALGORITHM_A128GCM = 1;
|
||||
|
||||
final public const COSE_ALGORITHM_A128KW = -3;
|
||||
|
||||
final public const COSE_ALGORITHM_A192KW = -4;
|
||||
|
||||
final public const COSE_ALGORITHM_A256KW = -5;
|
||||
|
||||
final public const COSE_ALGORITHM_DIRECT = -6;
|
||||
|
||||
final public const COSE_ALGORITHM_ES256 = -7;
|
||||
|
||||
/**
|
||||
* @deprecated since v4.0.6. Please use COSE_ALGORITHM_EDDSA instead. Will be removed in v5.0.0
|
||||
*/
|
||||
final public const COSE_ALGORITHM_EdDSA = -8;
|
||||
|
||||
final public const COSE_ALGORITHM_EDDSA = -8;
|
||||
|
||||
final public const COSE_ALGORITHM_ED256 = -260;
|
||||
|
||||
final public const COSE_ALGORITHM_ED512 = -261;
|
||||
|
||||
final public const COSE_ALGORITHM_DIRECT_HKDF_SHA_256 = -10;
|
||||
|
||||
final public const COSE_ALGORITHM_DIRECT_HKDF_SHA_512 = -11;
|
||||
|
||||
final public const COSE_ALGORITHM_DIRECT_HKDF_AES_128 = -12;
|
||||
|
||||
final public const COSE_ALGORITHM_DIRECT_HKDF_AES_256 = -13;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_ES_HKDF_256 = -25;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_ES_HKDF_512 = -26;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_SS_HKDF_256 = -27;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_SS_HKDF_512 = -28;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_ES_A128KW = -29;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_ES_A192KW = -30;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_ES_A256KW = -31;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_SS_A128KW = -32;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_SS_A192KW = -33;
|
||||
|
||||
final public const COSE_ALGORITHM_ECDH_SS_A256KW = -34;
|
||||
|
||||
final public const COSE_ALGORITHM_ES384 = -35;
|
||||
|
||||
final public const COSE_ALGORITHM_ES512 = -36;
|
||||
|
||||
final public const COSE_ALGORITHM_PS256 = -37;
|
||||
|
||||
final public const COSE_ALGORITHM_PS384 = -38;
|
||||
|
||||
final public const COSE_ALGORITHM_PS512 = -39;
|
||||
|
||||
final public const COSE_ALGORITHM_RSAES_OAEP = -40;
|
||||
|
||||
final public const COSE_ALGORITHM_RSAES_OAEP_256 = -41;
|
||||
|
||||
final public const COSE_ALGORITHM_RSAES_OAEP_512 = -42;
|
||||
|
||||
final public const COSE_ALGORITHM_ES256K = -46;
|
||||
|
||||
final public const COSE_ALGORITHM_RS256 = -257;
|
||||
|
||||
final public const COSE_ALGORITHM_RS384 = -258;
|
||||
|
||||
final public const COSE_ALGORITHM_RS512 = -259;
|
||||
|
||||
final public const COSE_ALGORITHM_RS1 = -65535;
|
||||
|
||||
final 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,
|
||||
];
|
||||
|
||||
final 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',
|
||||
];
|
||||
|
||||
public static function getOpensslAlgorithmFor(int $algorithmIdentifier): int
|
||||
{
|
||||
if (! array_key_exists($algorithmIdentifier, self::COSE_ALGORITHM_MAP)) {
|
||||
throw new InvalidArgumentException('The specified algorithm identifier is not supported');
|
||||
}
|
||||
|
||||
return self::COSE_ALGORITHM_MAP[$algorithmIdentifier];
|
||||
}
|
||||
|
||||
public static function getHashAlgorithmFor(int $algorithmIdentifier): string
|
||||
{
|
||||
if (! array_key_exists($algorithmIdentifier, self::COSE_HASH_MAP)) {
|
||||
throw new InvalidArgumentException('The specified algorithm identifier is not supported');
|
||||
}
|
||||
|
||||
return self::COSE_HASH_MAP[$algorithmIdentifier];
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose;
|
||||
|
||||
use Brick\Math\BigInteger as BrickBigInteger;
|
||||
use function chr;
|
||||
use function hex2bin;
|
||||
use function unpack;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BigInteger
|
||||
{
|
||||
private function __construct(
|
||||
private readonly BrickBigInteger $value
|
||||
) {
|
||||
}
|
||||
|
||||
public static function createFromBinaryString(string $value): self
|
||||
{
|
||||
$res = unpack('H*', $value);
|
||||
$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);
|
||||
|
||||
return ltrim($temp, chr(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two BigIntegers.
|
||||
*/
|
||||
public function add(self $y): self
|
||||
{
|
||||
$value = $this->value->plus($y->value);
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts two BigIntegers.
|
||||
*/
|
||||
public function subtract(self $y): self
|
||||
{
|
||||
$value = $this->value->minus($y->value);
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies two BigIntegers.
|
||||
*/
|
||||
public function multiply(self $x): self
|
||||
{
|
||||
$value = $this->value->multipliedBy($x->value);
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs modular exponentiation.
|
||||
*/
|
||||
public function modPow(self $e, self $n): self
|
||||
{
|
||||
$value = $this->value->modPow($e->value, $n->value);
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs modular exponentiation.
|
||||
*/
|
||||
public function mod(self $d): self
|
||||
{
|
||||
$value = $this->value->mod($d->value);
|
||||
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*/
|
||||
public function compare(self $y): int
|
||||
{
|
||||
return $this->value->compareTo($y->value);
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Hash
|
||||
{
|
||||
private function __construct(
|
||||
private readonly string $hash,
|
||||
private readonly int $length,
|
||||
private readonly string $t
|
||||
) {
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Key;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Primitive\ObjectIdentifier;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitlyTaggedType;
|
||||
use function array_key_exists;
|
||||
use function in_array;
|
||||
use function is_int;
|
||||
|
||||
/**
|
||||
* @final
|
||||
* @see \Cose\Tests\Key\Ec2KeyTest
|
||||
*/
|
||||
class Ec2Key extends Key
|
||||
{
|
||||
final public const CURVE_P256 = 1;
|
||||
|
||||
final public const CURVE_P256K = 8;
|
||||
|
||||
final public const CURVE_P384 = 2;
|
||||
|
||||
final public const CURVE_P521 = 3;
|
||||
|
||||
final public const CURVE_NAME_P256 = 'P-256';
|
||||
|
||||
final public const CURVE_NAME_P256K = 'P-256K';
|
||||
|
||||
final public const CURVE_NAME_P384 = 'P-384';
|
||||
|
||||
final public const CURVE_NAME_P521 = 'P-521';
|
||||
|
||||
final public const DATA_CURVE = -1;
|
||||
|
||||
final public const DATA_X = -2;
|
||||
|
||||
final public const DATA_Y = -3;
|
||||
|
||||
final public const DATA_D = -4;
|
||||
|
||||
private const SUPPORTED_CURVES_INT = [self::CURVE_P256, self::CURVE_P256K, self::CURVE_P384, self::CURVE_P521];
|
||||
|
||||
private const SUPPORTED_CURVES_NAMES = [
|
||||
self::CURVE_NAME_P256,
|
||||
self::CURVE_NAME_P256K,
|
||||
self::CURVE_NAME_P384,
|
||||
self::CURVE_NAME_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,
|
||||
self::CURVE_NAME_P256 => 32,
|
||||
self::CURVE_NAME_P256K => 32,
|
||||
self::CURVE_NAME_P384 => 48,
|
||||
self::CURVE_NAME_P521 => 66,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
foreach ([self::DATA_CURVE, self::TYPE] as $key) {
|
||||
if (is_numeric($data[$key])) {
|
||||
$data[$key] = (int) $data[$key];
|
||||
}
|
||||
}
|
||||
parent::__construct($data);
|
||||
if ($data[self::TYPE] !== self::TYPE_EC2 && $data[self::TYPE] !== self::TYPE_NAME_EC2) {
|
||||
throw new InvalidArgumentException('Invalid EC2 key. The key type does not correspond to an EC2 key');
|
||||
}
|
||||
if (! isset($data[self::DATA_CURVE], $data[self::DATA_X], $data[self::DATA_Y])) {
|
||||
throw new InvalidArgumentException('Invalid EC2 key. The curve or the "x/y" coordinates are missing');
|
||||
}
|
||||
if (mb_strlen((string) $data[self::DATA_X], '8bit') !== self::CURVE_KEY_LENGTH[$data[self::DATA_CURVE]]) {
|
||||
throw new InvalidArgumentException('Invalid length for x coordinate');
|
||||
}
|
||||
if (mb_strlen((string) $data[self::DATA_Y], '8bit') !== self::CURVE_KEY_LENGTH[$data[self::DATA_CURVE]]) {
|
||||
throw new InvalidArgumentException('Invalid length for y coordinate');
|
||||
}
|
||||
if (is_int($data[self::DATA_CURVE])) {
|
||||
if (! in_array($data[self::DATA_CURVE], self::SUPPORTED_CURVES_INT, true)) {
|
||||
throw new InvalidArgumentException('The curve is not supported');
|
||||
}
|
||||
} elseif (! in_array($data[self::DATA_CURVE], self::SUPPORTED_CURVES_NAMES, true)) {
|
||||
throw new InvalidArgumentException('The curve is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public static function create(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if (! $this->isPrivate()) {
|
||||
throw new InvalidArgumentException('The key is not private.');
|
||||
}
|
||||
return $this->get(self::DATA_D);
|
||||
}
|
||||
|
||||
public function curve(): int|string
|
||||
{
|
||||
return $this->get(self::DATA_CURVE);
|
||||
}
|
||||
|
||||
public function asPEM(): string
|
||||
{
|
||||
if ($this->isPrivate()) {
|
||||
$der = Sequence::create(
|
||||
Integer::create(1),
|
||||
OctetString::create($this->d()),
|
||||
ExplicitlyTaggedType::create(0, ObjectIdentifier::create($this->getCurveOid())),
|
||||
ExplicitlyTaggedType::create(1, BitString::create($this->getUncompressedCoordinates())),
|
||||
);
|
||||
|
||||
return $this->pem('EC PRIVATE KEY', $der->toDER());
|
||||
}
|
||||
|
||||
$der = Sequence::create(
|
||||
Sequence::create(
|
||||
ObjectIdentifier::create('1.2.840.10045.2.1'),
|
||||
ObjectIdentifier::create($this->getCurveOid())
|
||||
),
|
||||
BitString::create($this->getUncompressedCoordinates())
|
||||
);
|
||||
|
||||
return $this->pem('PUBLIC KEY', $der->toDER());
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Key;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function array_key_exists;
|
||||
|
||||
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 TYPE_NAME_OKP = 'OKP';
|
||||
|
||||
public const TYPE_NAME_EC2 = 'EC';
|
||||
|
||||
public const TYPE_NAME_RSA = 'RSA';
|
||||
|
||||
public const TYPE_NAME_OCT = 'oct';
|
||||
|
||||
public const KID = 2;
|
||||
|
||||
public const ALG = 3;
|
||||
|
||||
public const KEY_OPS = 4;
|
||||
|
||||
public const BASE_IV = 5;
|
||||
|
||||
/**
|
||||
* @var array<int|string, mixed>
|
||||
*/
|
||||
private readonly array $data;
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
if (! array_key_exists(self::TYPE, $data)) {
|
||||
throw new InvalidArgumentException('Invalid key: the type is not defined');
|
||||
}
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public static function create(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
public static function createFromData(array $data): self
|
||||
{
|
||||
if (! array_key_exists(self::TYPE, $data)) {
|
||||
throw new InvalidArgumentException('Invalid key: the type is not defined');
|
||||
}
|
||||
|
||||
return match ($data[self::TYPE]) {
|
||||
'1' => new OkpKey($data),
|
||||
'2' => new Ec2Key($data),
|
||||
'3' => new RsaKey($data),
|
||||
'4' => new SymmetricKey($data),
|
||||
default => self::create($data),
|
||||
};
|
||||
}
|
||||
|
||||
public function type(): int|string
|
||||
{
|
||||
return $this->data[self::TYPE];
|
||||
}
|
||||
|
||||
public function alg(): int
|
||||
{
|
||||
return (int) $this->get(self::ALG);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function has(int|string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->data);
|
||||
}
|
||||
|
||||
public function get(int|string $key): mixed
|
||||
{
|
||||
if (! array_key_exists($key, $this->data)) {
|
||||
throw new InvalidArgumentException(sprintf('The key has no data at index %d', $key));
|
||||
}
|
||||
|
||||
return $this->data[$key];
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Key;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use function array_key_exists;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @final
|
||||
* @see \Cose\Tests\Key\OkpKeyTest
|
||||
*/
|
||||
class OkpKey extends Key
|
||||
{
|
||||
final public const CURVE_X25519 = 4;
|
||||
|
||||
final public const CURVE_X448 = 5;
|
||||
|
||||
final public const CURVE_ED25519 = 6;
|
||||
|
||||
final public const CURVE_ED448 = 7;
|
||||
|
||||
final public const CURVE_NAME_X25519 = 'X25519';
|
||||
|
||||
final public const CURVE_NAME_X448 = 'X448';
|
||||
|
||||
final public const CURVE_NAME_ED25519 = 'Ed25519';
|
||||
|
||||
final public const CURVE_NAME_ED448 = 'Ed448';
|
||||
|
||||
final public const DATA_CURVE = -1;
|
||||
|
||||
final public const DATA_X = -2;
|
||||
|
||||
final public const DATA_D = -4;
|
||||
|
||||
private const SUPPORTED_CURVES_INT = [
|
||||
self::CURVE_X25519,
|
||||
self::CURVE_X448,
|
||||
self::CURVE_ED25519,
|
||||
self::CURVE_ED448,
|
||||
];
|
||||
|
||||
private const SUPPORTED_CURVES_NAME = [
|
||||
self::CURVE_NAME_X25519,
|
||||
self::CURVE_NAME_X448,
|
||||
self::CURVE_NAME_ED25519,
|
||||
self::CURVE_NAME_ED448,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
foreach ([self::DATA_CURVE, self::TYPE] as $key) {
|
||||
if (is_numeric($data[$key])) {
|
||||
$data[$key] = (int) $data[$key];
|
||||
}
|
||||
}
|
||||
parent::__construct($data);
|
||||
if ($data[self::TYPE] !== self::TYPE_OKP && $data[self::TYPE] !== self::TYPE_NAME_OKP) {
|
||||
throw new InvalidArgumentException('Invalid OKP key. The key type does not correspond to an OKP key');
|
||||
}
|
||||
if (! isset($data[self::DATA_CURVE], $data[self::DATA_X])) {
|
||||
throw new InvalidArgumentException('Invalid EC2 key. The curve or the "x" coordinate is missing');
|
||||
}
|
||||
if (is_numeric($data[self::DATA_CURVE])) {
|
||||
if (! in_array((int) $data[self::DATA_CURVE], self::SUPPORTED_CURVES_INT, true)) {
|
||||
throw new InvalidArgumentException('The curve is not supported');
|
||||
}
|
||||
} elseif (! in_array($data[self::DATA_CURVE], self::SUPPORTED_CURVES_NAME, true)) {
|
||||
throw new InvalidArgumentException('The curve is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public static function create(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if (! $this->isPrivate()) {
|
||||
throw new InvalidArgumentException('The key is not private.');
|
||||
}
|
||||
|
||||
return $this->get(self::DATA_D);
|
||||
}
|
||||
|
||||
public function curve(): int|string
|
||||
{
|
||||
return $this->get(self::DATA_CURVE);
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Key;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
use InvalidArgumentException;
|
||||
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PublicKeyInfo;
|
||||
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RSA\RSAPrivateKey;
|
||||
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RSA\RSAPublicKey;
|
||||
use function array_key_exists;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @final
|
||||
* @see \Cose\Tests\Key\RsaKeyTest
|
||||
*/
|
||||
class RsaKey extends Key
|
||||
{
|
||||
final public const DATA_N = -1;
|
||||
|
||||
final public const DATA_E = -2;
|
||||
|
||||
final public const DATA_D = -3;
|
||||
|
||||
final public const DATA_P = -4;
|
||||
|
||||
final public const DATA_Q = -5;
|
||||
|
||||
final public const DATA_DP = -6;
|
||||
|
||||
final public const DATA_DQ = -7;
|
||||
|
||||
final public const DATA_QI = -8;
|
||||
|
||||
final public const DATA_OTHER = -9;
|
||||
|
||||
final public const DATA_RI = -10;
|
||||
|
||||
final public const DATA_DI = -11;
|
||||
|
||||
final public const DATA_TI = -12;
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
foreach ([self::TYPE] as $key) {
|
||||
if (is_numeric($data[$key])) {
|
||||
$data[$key] = (int) $data[$key];
|
||||
}
|
||||
}
|
||||
parent::__construct($data);
|
||||
if ($data[self::TYPE] !== self::TYPE_RSA && $data[self::TYPE] !== self::TYPE_NAME_RSA) {
|
||||
throw new InvalidArgumentException('Invalid RSA key. The key type does not correspond to a RSA key');
|
||||
}
|
||||
if (! isset($data[self::DATA_N], $data[self::DATA_E])) {
|
||||
throw new InvalidArgumentException('Invalid RSA key. The modulus or the exponent is missing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public static function create(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
public function n(): string
|
||||
{
|
||||
return $this->get(self::DATA_N);
|
||||
}
|
||||
|
||||
public function e(): string
|
||||
{
|
||||
return $this->get(self::DATA_E);
|
||||
}
|
||||
|
||||
public function d(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_D);
|
||||
}
|
||||
|
||||
public function p(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_P);
|
||||
}
|
||||
|
||||
public function q(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_Q);
|
||||
}
|
||||
|
||||
public function dP(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_DP);
|
||||
}
|
||||
|
||||
public function dQ(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_DQ);
|
||||
}
|
||||
|
||||
public function QInv(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_QI);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function other(): array
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_OTHER);
|
||||
}
|
||||
|
||||
public function rI(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_RI);
|
||||
}
|
||||
|
||||
public function dI(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_DI);
|
||||
}
|
||||
|
||||
public function tI(): string
|
||||
{
|
||||
$this->checkKeyIsPrivate();
|
||||
|
||||
return $this->get(self::DATA_TI);
|
||||
}
|
||||
|
||||
public function hasPrimes(): bool
|
||||
{
|
||||
return $this->has(self::DATA_P) && $this->has(self::DATA_Q);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function primes(): array
|
||||
{
|
||||
return [$this->p(), $this->q()];
|
||||
}
|
||||
|
||||
public function hasExponents(): bool
|
||||
{
|
||||
return $this->has(self::DATA_DP) && $this->has(self::DATA_DQ);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
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
|
||||
{
|
||||
if ($this->isPrivate()) {
|
||||
$privateKey = RSAPrivateKey::create(
|
||||
$this->binaryToBigInteger($this->n()),
|
||||
$this->binaryToBigInteger($this->e()),
|
||||
$this->binaryToBigInteger($this->d()),
|
||||
$this->binaryToBigInteger($this->p()),
|
||||
$this->binaryToBigInteger($this->q()),
|
||||
$this->binaryToBigInteger($this->dP()),
|
||||
$this->binaryToBigInteger($this->dQ()),
|
||||
$this->binaryToBigInteger($this->QInv())
|
||||
);
|
||||
|
||||
return $privateKey->toPEM()
|
||||
->string();
|
||||
}
|
||||
|
||||
$publicKey = RSAPublicKey::create(
|
||||
$this->binaryToBigInteger($this->n()),
|
||||
$this->binaryToBigInteger($this->e())
|
||||
);
|
||||
$rsaKey = PublicKeyInfo::fromPublicKey($publicKey);
|
||||
|
||||
return $rsaKey->toPEM()
|
||||
->string();
|
||||
}
|
||||
|
||||
public function toPublic(): static
|
||||
{
|
||||
$toBeRemoved = [
|
||||
self::DATA_D,
|
||||
self::DATA_P,
|
||||
self::DATA_Q,
|
||||
self::DATA_DP,
|
||||
self::DATA_DQ,
|
||||
self::DATA_QI,
|
||||
self::DATA_OTHER,
|
||||
self::DATA_RI,
|
||||
self::DATA_DI,
|
||||
self::DATA_TI,
|
||||
];
|
||||
$data = $this->getData();
|
||||
foreach ($data as $k => $v) {
|
||||
if (in_array($k, $toBeRemoved, true)) {
|
||||
unset($data[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
return new static($data);
|
||||
}
|
||||
|
||||
private function checkKeyIsPrivate(): void
|
||||
{
|
||||
if (! $this->isPrivate()) {
|
||||
throw new InvalidArgumentException('The key is not private.');
|
||||
}
|
||||
}
|
||||
|
||||
private function binaryToBigInteger(string $data): string
|
||||
{
|
||||
$res = unpack('H*', $data);
|
||||
$res = current($res);
|
||||
|
||||
return BigInteger::fromBase($res, 16)->toBase(10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cose\Key;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class SymmetricKey extends Key
|
||||
{
|
||||
final public const DATA_K = -1;
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public function __construct(array $data)
|
||||
{
|
||||
parent::__construct($data);
|
||||
if (! isset($data[self::TYPE]) || (int) $data[self::TYPE] !== self::TYPE_OCT) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid symmetric key. The key type does not correspond to a symmetric key'
|
||||
);
|
||||
}
|
||||
if (! isset($data[self::DATA_K])) {
|
||||
throw new InvalidArgumentException('Invalid symmetric key. The parameter "k" is missing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $data
|
||||
*/
|
||||
public static function create(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
public function k(): string
|
||||
{
|
||||
return $this->get(self::DATA_K);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2022 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.
|
||||
Vendored
+227
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
|
||||
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function openssl_pkey_get_public;
|
||||
use function openssl_verify;
|
||||
|
||||
final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->decoder = Decoder::create();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'android-key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create($attestation);
|
||||
foreach (['sig', 'x5c', 'alg'] as $key) {
|
||||
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
sprintf('The attestation statement value "%s" is missing.', $key)
|
||||
);
|
||||
}
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
(is_countable($certificates) ? count(
|
||||
$certificates
|
||||
) : 0) > 0 || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "x5c" must be a list with at least one certificate.'
|
||||
);
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
|
||||
$attestationStatement = AttestationStatement::createBasic(
|
||||
$attestation['fmt'],
|
||||
$attestation['attStmt'],
|
||||
CertificateTrustPath::create($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path. Shall contain certificates.'
|
||||
);
|
||||
|
||||
$certificates = $trustPath->certificates;
|
||||
|
||||
//Decode leaf attestation certificate
|
||||
$leaf = $certificates[0];
|
||||
$this->checkCertificate($leaf, $clientDataJSONHash, $authenticatorData);
|
||||
|
||||
$signedData = $authenticatorData->authData . $clientDataJSONHash;
|
||||
$alg = $attestationStatement->get('alg');
|
||||
|
||||
return openssl_verify(
|
||||
$signedData,
|
||||
$attestationStatement->get('sig'),
|
||||
$leaf,
|
||||
Algorithms::getOpensslAlgorithmFor((int) $alg)
|
||||
) === 1;
|
||||
}
|
||||
|
||||
private function checkCertificate(
|
||||
string $certificate,
|
||||
string $clientDataHash,
|
||||
AuthenticatorData $authenticatorData
|
||||
): void {
|
||||
$resource = openssl_pkey_get_public($certificate);
|
||||
$details = openssl_pkey_get_details($resource);
|
||||
is_array($details) || throw AttestationStatementVerificationException::create(
|
||||
'Unable to read the certificate'
|
||||
);
|
||||
|
||||
//Check that authData publicKey matches the public key in the attestation certificate
|
||||
$attestedCredentialData = $authenticatorData->attestedCredentialData;
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential data found'
|
||||
);
|
||||
$publicKeyData = $attestedCredentialData->credentialPublicKey;
|
||||
$publicKeyData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested public key found'
|
||||
);
|
||||
$publicDataStream = new StringStream($publicKeyData);
|
||||
$coseKey = $this->decoder->decode($publicDataStream);
|
||||
$coseKey instanceof Normalizable || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attested public key found'
|
||||
);
|
||||
|
||||
$publicDataStream->isEOF() || throw AttestationStatementVerificationException::create(
|
||||
'Invalid public key data. Presence of extra bytes.'
|
||||
);
|
||||
$publicDataStream->close();
|
||||
$publicKey = Key::createFromData($coseKey->normalize());
|
||||
|
||||
($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create(
|
||||
'Unsupported key type'
|
||||
);
|
||||
$publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create(
|
||||
'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
|
||||
is_array(
|
||||
$certDetails
|
||||
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
|
||||
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate has no extension'
|
||||
);
|
||||
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate has no extension'
|
||||
);
|
||||
array_key_exists(
|
||||
'1.3.6.1.4.1.11129.2.1.17',
|
||||
$certDetails['extensions']
|
||||
) || throw AttestationStatementVerificationException::create(
|
||||
'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 = Sequence::fromDER($extension);
|
||||
$extensionAsAsn1->has(4);
|
||||
|
||||
//Check that attestationChallenge is set to the clientDataHash.
|
||||
$extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
|
||||
);
|
||||
$ext = $extensionAsAsn1->at(4)
|
||||
->asElement();
|
||||
$ext instanceof OctetString || throw AttestationStatementVerificationException::create(
|
||||
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
|
||||
);
|
||||
$clientDataHash === $ext->string() || throw AttestationStatementVerificationException::create(
|
||||
'The client data hash is not valid'
|
||||
);
|
||||
|
||||
//Check that both teeEnforced and softwareEnforced structures don't contain allApplications(600) tag.
|
||||
$extensionAsAsn1->has(6) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
|
||||
);
|
||||
|
||||
$softwareEnforcedFlags = $extensionAsAsn1->at(6)
|
||||
->asElement();
|
||||
$softwareEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
|
||||
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
|
||||
);
|
||||
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
|
||||
|
||||
$extensionAsAsn1->has(7) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
|
||||
);
|
||||
$teeEnforcedFlags = $extensionAsAsn1->at(7)
|
||||
->asElement();
|
||||
$teeEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
|
||||
'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->elements() as $tag) {
|
||||
$tag->asElement() instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
|
||||
'Invalid tag'
|
||||
);
|
||||
$tag->asElement()
|
||||
->tag() !== 600 || throw AttestationStatementVerificationException::create('Forbidden tag 600 found');
|
||||
}
|
||||
}
|
||||
}
|
||||
+382
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use Jose\Component\Core\Algorithm as AlgorithmInterface;
|
||||
use Jose\Component\Core\AlgorithmManager;
|
||||
use Jose\Component\KeyManagement\JWKFactory;
|
||||
use Jose\Component\Signature\Algorithm\EdDSA;
|
||||
use Jose\Component\Signature\Algorithm\ES256;
|
||||
use Jose\Component\Signature\Algorithm\ES384;
|
||||
use Jose\Component\Signature\Algorithm\ES512;
|
||||
use Jose\Component\Signature\Algorithm\PS256;
|
||||
use Jose\Component\Signature\Algorithm\PS384;
|
||||
use Jose\Component\Signature\Algorithm\PS512;
|
||||
use Jose\Component\Signature\Algorithm\RS256;
|
||||
use Jose\Component\Signature\Algorithm\RS384;
|
||||
use Jose\Component\Signature\Algorithm\RS512;
|
||||
use Jose\Component\Signature\JWS;
|
||||
use Jose\Component\Signature\JWSVerifier;
|
||||
use Jose\Component\Signature\Serializer\CompactSerializer;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\Exception\UnsupportedFeatureException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
* @deprecated since 4.9.0 and will be removed in 5.0.0. Android SafetyNet is now deprecated.
|
||||
*/
|
||||
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private ?string $apiKey = null;
|
||||
|
||||
private null|ClientInterface|HttpClientInterface $client = null;
|
||||
|
||||
private readonly CompactSerializer $jwsSerializer;
|
||||
|
||||
private ?JWSVerifier $jwsVerifier = null;
|
||||
|
||||
private ?RequestFactoryInterface $requestFactory = null;
|
||||
|
||||
private int $leeway = 0;
|
||||
|
||||
private int $maxAge = 60000;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
private readonly null|ClockInterface $clock = null
|
||||
) {
|
||||
if ($this->clock === null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$clock" will be required in 5.0.0. Please set a clock instance.'
|
||||
);
|
||||
}
|
||||
if (! class_exists(RS256::class) || ! class_exists(JWKFactory::class)) {
|
||||
throw UnsupportedFeatureException::create(
|
||||
'The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-library?'
|
||||
);
|
||||
}
|
||||
$this->jwsSerializer = new CompactSerializer();
|
||||
$this->initJwsVerifier();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(null|ClockInterface $clock = null): self
|
||||
{
|
||||
return new self($clock);
|
||||
}
|
||||
|
||||
public function enableApiVerification(
|
||||
ClientInterface|HttpClientInterface $client,
|
||||
string $apiKey,
|
||||
?RequestFactoryInterface $requestFactory = null
|
||||
): self {
|
||||
$this->apiKey = $apiKey;
|
||||
$this->client = $client;
|
||||
$this->requestFactory = $requestFactory;
|
||||
if ($requestFactory !== null && ! $client instanceof HttpClientInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/metadata-service',
|
||||
'4.7.0',
|
||||
'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$client" argument.'
|
||||
);
|
||||
}
|
||||
|
||||
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 array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
|
||||
$attestation
|
||||
);
|
||||
foreach (['ver', 'response'] as $key) {
|
||||
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
sprintf('The attestation statement value "%s" is missing.', $key)
|
||||
);
|
||||
$attestation['attStmt'][$key] !== '' || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
sprintf('The attestation statement value "%s" is empty.', $key)
|
||||
);
|
||||
}
|
||||
$jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
|
||||
$jwsHeader = $jws->getSignature(0)
|
||||
->getProtectedHeader();
|
||||
array_key_exists('x5c', $jwsHeader) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The response in the attestation statement must contain a "x5c" header.'
|
||||
);
|
||||
(is_countable($jwsHeader['x5c']) ? count(
|
||||
$jwsHeader['x5c']
|
||||
) : 0) > 0 || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The "x5c" parameter in the attestation statement response must contain at least one certificate.'
|
||||
);
|
||||
$certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
|
||||
$attestation['attStmt']['jws'] = $jws;
|
||||
|
||||
$attestationStatement = AttestationStatement::createBasic(
|
||||
$this->name(),
|
||||
$attestation['attStmt'],
|
||||
CertificateTrustPath::create($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path'
|
||||
);
|
||||
$certificates = $trustPath->certificates;
|
||||
$firstCertificate = current($certificates);
|
||||
is_string($firstCertificate) || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'No certificate'
|
||||
);
|
||||
|
||||
$parsedCertificate = openssl_x509_parse($firstCertificate);
|
||||
is_array($parsedCertificate) || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
array_key_exists('subject', $parsedCertificate) || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
array_key_exists('CN', $parsedCertificate['subject']) || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
$parsedCertificate['subject']['CN'] === 'attest.android.com' || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'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 {
|
||||
$payload !== null || throw AttestationStatementVerificationException::create('Invalid attestation object');
|
||||
$payload = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
|
||||
array_key_exists('nonce', $payload) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attestation object. "nonce" is missing.'
|
||||
);
|
||||
$payload['nonce'] === base64_encode(
|
||||
hash('sha256', $authenticatorData->authData . $clientDataJSONHash, true)
|
||||
) || throw AttestationStatementVerificationException::create('Invalid attestation object. Invalid nonce');
|
||||
array_key_exists('ctsProfileMatch', $payload) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attestation object. "ctsProfileMatch" is missing.'
|
||||
);
|
||||
$payload['ctsProfileMatch'] || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attestation object. "ctsProfileMatch" value is false.'
|
||||
);
|
||||
array_key_exists('timestampMs', $payload) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attestation object. Timestamp is missing.'
|
||||
);
|
||||
is_int($payload['timestampMs']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attestation object. Timestamp shall be an integer.'
|
||||
);
|
||||
|
||||
$currentTime = ($this->clock?->now()->getTimestamp() ?? time()) * 1000;
|
||||
$payload['timestampMs'] <= $currentTime + $this->leeway || throw AttestationStatementVerificationException::create(
|
||||
sprintf(
|
||||
'Invalid attestation object. Issued in the future. Current time: %d. Response time: %d',
|
||||
$currentTime,
|
||||
$payload['timestampMs']
|
||||
)
|
||||
);
|
||||
$currentTime - $payload['timestampMs'] <= $this->maxAge || throw AttestationStatementVerificationException::create(
|
||||
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->certificates[0]);
|
||||
$isValid = $this->jwsVerifier?->verifyWithKey($jws, $jwk, 0);
|
||||
$isValid === true || throw AttestationStatementVerificationException::create('Invalid response signature');
|
||||
}
|
||||
|
||||
private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
|
||||
{
|
||||
if ($this->client === null || $this->apiKey === null) {
|
||||
return;
|
||||
}
|
||||
$uri = sprintf(
|
||||
'https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s',
|
||||
urlencode($this->apiKey)
|
||||
);
|
||||
$requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
|
||||
if ($this->client instanceof HttpClientInterface) {
|
||||
$responseBody = $this->validateUsingGoogleApiWithSymfonyClient($requestBody, $uri);
|
||||
} else {
|
||||
$responseBody = $this->validateUsingGoogleApiWithPsrClient($requestBody, $uri);
|
||||
}
|
||||
$responseBodyJson = json_decode($responseBody, true, flags: JSON_THROW_ON_ERROR);
|
||||
array_key_exists(
|
||||
'isValidSignature',
|
||||
$responseBodyJson
|
||||
) || throw AttestationStatementVerificationException::create('Invalid response.');
|
||||
$responseBodyJson['isValidSignature'] === true || throw AttestationStatementVerificationException::create(
|
||||
'Invalid response.'
|
||||
);
|
||||
}
|
||||
|
||||
private function getResponseBody(ResponseInterface $response): string
|
||||
{
|
||||
$responseBody = '';
|
||||
$response->getBody()
|
||||
->rewind();
|
||||
do {
|
||||
$tmp = $response->getBody()
|
||||
->read(1024);
|
||||
if ($tmp === '') {
|
||||
break;
|
||||
}
|
||||
$responseBody .= $tmp;
|
||||
} while (true);
|
||||
|
||||
return $responseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = [
|
||||
RS256::class, RS384::class, RS512::class,
|
||||
PS256::class, PS384::class, PS512::class,
|
||||
ES256::class, ES384::class, ES512::class,
|
||||
EdDSA::class,
|
||||
];
|
||||
/** @var AlgorithmInterface[] $algorithms */
|
||||
$algorithms = [];
|
||||
foreach ($algorithmClasses as $algorithm) {
|
||||
if (class_exists($algorithm)) {
|
||||
$algorithms[] = new $algorithm();
|
||||
}
|
||||
}
|
||||
$algorithmManager = new AlgorithmManager($algorithms);
|
||||
$this->jwsVerifier = new JWSVerifier($algorithmManager);
|
||||
}
|
||||
|
||||
private function validateUsingGoogleApiWithSymfonyClient(string $requestBody, string $uri): string
|
||||
{
|
||||
$response = $this->client->request('POST', $uri, [
|
||||
'headers' => [
|
||||
'content-type' => 'application/json',
|
||||
],
|
||||
'body' => $requestBody,
|
||||
]);
|
||||
$response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create(
|
||||
'Request did not succeeded'
|
||||
);
|
||||
|
||||
return $response->getContent();
|
||||
}
|
||||
|
||||
private function validateUsingGoogleApiWithPsrClient(string $requestBody, string $uri): string
|
||||
{
|
||||
$request = $this->requestFactory->createRequest('POST', $uri);
|
||||
$request = $request->withHeader('content-type', 'application/json');
|
||||
$request->getBody()
|
||||
->write($requestBody);
|
||||
|
||||
$response = $this->client->sendRequest($request);
|
||||
$response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create(
|
||||
'Request did not succeeded'
|
||||
);
|
||||
|
||||
return $this->getResponseBody($response);
|
||||
}
|
||||
}
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function openssl_pkey_get_public;
|
||||
|
||||
final class AppleAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->decoder = Decoder::create();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'apple';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
array_key_exists('x5c', $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "x5c" is missing.'
|
||||
);
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
(is_countable($certificates) ? count(
|
||||
$certificates
|
||||
) : 0) > 0 || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "x5c" must be a list with at least one certificate.'
|
||||
);
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
|
||||
$attestationStatement = AttestationStatement::createAnonymizationCA(
|
||||
$attestation['fmt'],
|
||||
$attestation['attStmt'],
|
||||
CertificateTrustPath::create($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path'
|
||||
);
|
||||
|
||||
$certificates = $trustPath->certificates;
|
||||
|
||||
//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);
|
||||
is_array($details) || throw AttestationStatementVerificationException::create(
|
||||
'Unable to read the certificate'
|
||||
);
|
||||
|
||||
//Check that authData publicKey matches the public key in the attestation certificate
|
||||
$attestedCredentialData = $authenticatorData->attestedCredentialData;
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential data found'
|
||||
);
|
||||
$publicKeyData = $attestedCredentialData->credentialPublicKey;
|
||||
$publicKeyData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested public key found'
|
||||
);
|
||||
$publicDataStream = new StringStream($publicKeyData);
|
||||
$coseKey = $this->decoder->decode($publicDataStream);
|
||||
$coseKey instanceof Normalizable || throw AttestationStatementVerificationException::create(
|
||||
'Invalid attested public key found'
|
||||
);
|
||||
$publicDataStream->isEOF() || throw AttestationStatementVerificationException::create(
|
||||
'Invalid public key data. Presence of extra bytes.'
|
||||
);
|
||||
$publicDataStream->close();
|
||||
$publicKey = Key::createFromData($coseKey->normalize());
|
||||
|
||||
($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create(
|
||||
'Unsupported key type'
|
||||
);
|
||||
|
||||
//We check the attested key corresponds to the key in the certificate
|
||||
$publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create(
|
||||
'Invalid key'
|
||||
);
|
||||
|
||||
/*---------------------------*/
|
||||
$certDetails = openssl_x509_parse($certificate);
|
||||
|
||||
//Find Apple Extension with OID "1.2.840.113635.100.8.2" in certificate extensions
|
||||
is_array(
|
||||
$certDetails
|
||||
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
|
||||
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate has no extension'
|
||||
);
|
||||
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate has no extension'
|
||||
);
|
||||
array_key_exists(
|
||||
'1.2.840.113635.100.8.2',
|
||||
$certDetails['extensions']
|
||||
) || throw AttestationStatementVerificationException::create(
|
||||
'The certificate extension "1.2.840.113635.100.8.2" is missing'
|
||||
);
|
||||
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
|
||||
|
||||
$nonceToHash = $authenticatorData->authData . $clientDataHash;
|
||||
$nonce = hash('sha256', $nonceToHash);
|
||||
|
||||
//'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object
|
||||
'3024a1220420' . $nonce === bin2hex(
|
||||
(string) $extension
|
||||
) || throw AttestationStatementVerificationException::create('The client data hash is not valid');
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\MetadataService\Statement\MetadataStatement;
|
||||
|
||||
class AttestationObject
|
||||
{
|
||||
public ?MetadataStatement $metadataStatement = null;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $rawAttestationObject,
|
||||
public AttestationStatement $attStmt,
|
||||
public readonly AuthenticatorData $authData
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(
|
||||
string $rawAttestationObject,
|
||||
AttestationStatement $attStmt,
|
||||
AuthenticatorData $authData
|
||||
): self {
|
||||
return new self($rawAttestationObject, $attStmt, $authData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getRawAttestationObject(): string
|
||||
{
|
||||
return $this->rawAttestationObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAttStmt(): AttestationStatement
|
||||
{
|
||||
return $this->attStmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setAttStmt(AttestationStatement $attStmt): void
|
||||
{
|
||||
$this->attStmt = $attStmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAuthData(): AuthenticatorData
|
||||
{
|
||||
return $this->authData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getMetadataStatement(): ?MetadataStatement
|
||||
{
|
||||
return $this->metadataStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setMetadataStatement(MetadataStatement $metadataStatement): self
|
||||
{
|
||||
$this->metadataStatement = $metadataStatement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticatorDataLoader;
|
||||
use Webauthn\Event\AttestationObjectLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\Util\Base64;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
|
||||
class AttestationObjectLoader implements CanDispatchEvents, CanLogData
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
private readonly AttestationStatementSupportManager $attestationStatementSupportManager
|
||||
) {
|
||||
$this->logger = new NullLogger();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
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 = Base64::decode($data);
|
||||
$stream = new StringStream($decodedData);
|
||||
$parsed = Decoder::create()->decode($stream);
|
||||
|
||||
$this->logger->info('Loading the Attestation Statement');
|
||||
$parsed instanceof Normalizable || throw InvalidDataException::create(
|
||||
$parsed,
|
||||
'Invalid attestation object. Unexpected object.'
|
||||
);
|
||||
$attestationObject = $parsed->normalize();
|
||||
$stream->isEOF() || throw InvalidDataException::create(
|
||||
null,
|
||||
'Invalid attestation object. Presence of extra bytes.'
|
||||
);
|
||||
$stream->close();
|
||||
is_array($attestationObject) || throw InvalidDataException::create(
|
||||
$attestationObject,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
array_key_exists('authData', $attestationObject) || throw InvalidDataException::create(
|
||||
$attestationObject,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
array_key_exists('fmt', $attestationObject) || throw InvalidDataException::create(
|
||||
$attestationObject,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
array_key_exists('attStmt', $attestationObject) || throw InvalidDataException::create(
|
||||
$attestationObject,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
|
||||
$attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']);
|
||||
$attestationStatement = $attestationStatementSupport->load($attestationObject);
|
||||
$this->logger->info('Attestation Statement loaded');
|
||||
$this->logger->debug('Attestation Statement loaded', [
|
||||
'attestationStatement' => $attestationStatement,
|
||||
]);
|
||||
$authData = $attestationObject['authData'];
|
||||
$authDataLoader = AuthenticatorDataLoader::create();
|
||||
$authenticatorData = $authDataLoader->load($authData);
|
||||
|
||||
$attestationObject = AttestationObject::create($data, $attestationStatement, $authenticatorData);
|
||||
$this->logger->info('Attestation Object loaded');
|
||||
$this->logger->debug('Attestation Object', [
|
||||
'ed' => $attestationObject,
|
||||
]);
|
||||
$this->dispatcher->dispatch(AttestationObjectLoaded::create($attestationObject));
|
||||
|
||||
return $attestationObject;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\TrustPath\TrustPath;
|
||||
use Webauthn\TrustPath\TrustPathLoader;
|
||||
use function array_key_exists;
|
||||
|
||||
class AttestationStatement implements JsonSerializable
|
||||
{
|
||||
final public const TYPE_NONE = 'none';
|
||||
|
||||
final public const TYPE_BASIC = 'basic';
|
||||
|
||||
final public const TYPE_SELF = 'self';
|
||||
|
||||
final public const TYPE_ATTCA = 'attca';
|
||||
|
||||
/**
|
||||
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
final public const TYPE_ECDAA = 'ecdaa';
|
||||
|
||||
final public const TYPE_ANONCA = 'anonca';
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $fmt,
|
||||
public readonly array $attStmt,
|
||||
public readonly string $type,
|
||||
public readonly TrustPath $trustPath
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(string $fmt, array $attStmt, string $type, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, $type, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return self::create($fmt, $attStmt, self::TYPE_NONE, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return self::create($fmt, $attStmt, self::TYPE_BASIC, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return self::create($fmt, $attStmt, self::TYPE_SELF, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return self::create($fmt, $attStmt, self::TYPE_ATTCA, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*
|
||||
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return self::create($fmt, $attStmt, self::TYPE_ECDAA, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createAnonymizationCA(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return self::create($fmt, $attStmt, self::TYPE_ANONCA, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getFmt(): string
|
||||
{
|
||||
return $this->fmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAttStmt(): array
|
||||
{
|
||||
return $this->attStmt;
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->attStmt);
|
||||
}
|
||||
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
$this->has($key) || throw InvalidDataException::create($this->attStmt, sprintf(
|
||||
'The attestation statement has no key "%s".',
|
||||
$key
|
||||
));
|
||||
|
||||
return $this->attStmt[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getTrustPath(): TrustPath
|
||||
{
|
||||
return $this->trustPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public static function createFromArray(array $data): self
|
||||
{
|
||||
foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) {
|
||||
array_key_exists($key, $data) || throw InvalidDataException::create($data, sprintf(
|
||||
'The key "%s" is missing',
|
||||
$key
|
||||
));
|
||||
}
|
||||
|
||||
return self::create(
|
||||
$data['fmt'],
|
||||
$data['attStmt'],
|
||||
$data['type'],
|
||||
TrustPathLoader::loadTrustPath($data['trustPath'])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-bundle',
|
||||
'4.9.0',
|
||||
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
|
||||
__METHOD__
|
||||
);
|
||||
return [
|
||||
'fmt' => $this->fmt,
|
||||
'attStmt' => $this->attStmt,
|
||||
'trustPath' => $this->trustPath,
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use Webauthn\AuthenticatorData;
|
||||
|
||||
interface AttestationStatementSupport
|
||||
{
|
||||
public function name(): string;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement;
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool;
|
||||
}
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use function array_key_exists;
|
||||
|
||||
class AttestationStatementSupportManager
|
||||
{
|
||||
/**
|
||||
* @param AttestationStatementSupport[] $attestationStatementSupports
|
||||
*/
|
||||
public function __construct(
|
||||
private array $attestationStatementSupports = []
|
||||
) {
|
||||
$this->add(new NoneAttestationStatementSupport());
|
||||
foreach ($attestationStatementSupports as $attestationStatementSupport) {
|
||||
$this->add($attestationStatementSupport);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AttestationStatementSupport[] $attestationStatementSupports
|
||||
*/
|
||||
public static function create(array $attestationStatementSupports = []): self
|
||||
{
|
||||
return new self($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
|
||||
{
|
||||
$this->has($name) || throw InvalidDataException::create($name, sprintf(
|
||||
'The attestation statement format "%s" is not supported.',
|
||||
$name
|
||||
));
|
||||
|
||||
return $this->attestationStatementSupports[$name];
|
||||
}
|
||||
}
|
||||
Vendored
+181
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function openssl_pkey_get_public;
|
||||
use function openssl_verify;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
|
||||
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->decoder = Decoder::create();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'fido-u2f';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
foreach (['sig', 'x5c'] as $key) {
|
||||
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
sprintf('The attestation statement value "%s" is missing.', $key)
|
||||
);
|
||||
}
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
is_array($certificates) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "x5c" must be a list with one certificate.'
|
||||
);
|
||||
count($certificates) === 1 || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "x5c" must be a list with one certificate.'
|
||||
);
|
||||
|
||||
reset($certificates);
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
$this->checkCertificate($certificates[0]);
|
||||
|
||||
$attestationStatement = AttestationStatement::createBasic(
|
||||
$attestation['fmt'],
|
||||
$attestation['attStmt'],
|
||||
CertificateTrustPath::create($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$authenticatorData->attestedCredentialData
|
||||
?->aaguid
|
||||
->__toString() === '00000000-0000-0000-0000-000000000000' || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"'
|
||||
);
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path'
|
||||
);
|
||||
$dataToVerify = "\0";
|
||||
$dataToVerify .= $authenticatorData->rpIdHash;
|
||||
$dataToVerify .= $clientDataJSONHash;
|
||||
$dataToVerify .= $authenticatorData->attestedCredentialData
|
||||
->credentialId;
|
||||
$dataToVerify .= $this->extractPublicKey($authenticatorData->attestedCredentialData ->credentialPublicKey);
|
||||
|
||||
return openssl_verify(
|
||||
$dataToVerify,
|
||||
$attestationStatement->get('sig'),
|
||||
$trustPath->certificates[0],
|
||||
OPENSSL_ALGO_SHA256
|
||||
) === 1;
|
||||
}
|
||||
|
||||
private function extractPublicKey(?string $publicKey): string
|
||||
{
|
||||
$publicKey !== null || throw AttestationStatementVerificationException::create(
|
||||
'The attested credential data does not contain a valid public key.'
|
||||
);
|
||||
|
||||
$publicKeyStream = new StringStream($publicKey);
|
||||
$coseKey = $this->decoder->decode($publicKeyStream);
|
||||
$publicKeyStream->isEOF() || throw AttestationStatementVerificationException::create(
|
||||
'Invalid public key. Presence of extra bytes.'
|
||||
);
|
||||
$publicKeyStream->close();
|
||||
$coseKey instanceof MapObject || throw AttestationStatementVerificationException::create(
|
||||
'The attested credential data does not contain a valid public key.'
|
||||
);
|
||||
|
||||
$coseKey = $coseKey->normalize();
|
||||
$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 AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain',
|
||||
$throwable
|
||||
);
|
||||
}
|
||||
is_array($details) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain'
|
||||
);
|
||||
array_key_exists('ec', $details) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain'
|
||||
);
|
||||
array_key_exists('curve_name', $details['ec']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain'
|
||||
);
|
||||
$details['ec']['curve_name'] === 'prime256v1' || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain'
|
||||
);
|
||||
array_key_exists('curve_oid', $details['ec']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain'
|
||||
);
|
||||
$details['ec']['curve_oid'] === '1.2.840.10045.3.1.7' || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate or certificate chain'
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
final class NoneAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
$format = $attestation['fmt'] ?? null;
|
||||
$attestationStatement = $attestation['attStmt'] ?? [];
|
||||
|
||||
(is_string($format) && $format !== '') || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
(is_array(
|
||||
$attestationStatement
|
||||
) && $attestationStatement === []) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
|
||||
$attestationStatement = AttestationStatement::createNone(
|
||||
$format,
|
||||
$attestationStatement,
|
||||
EmptyTrustPath::create()
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
return count($attestationStatement->attStmt) === 0;
|
||||
}
|
||||
}
|
||||
Vendored
+303
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use Cose\Algorithm\Manager;
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Key;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\Exception\UnsupportedFeatureException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
use Webauthn\Util\CoseSignatureFixer;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function openssl_verify;
|
||||
|
||||
final class PackedAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
private readonly Manager $algorithmManager
|
||||
) {
|
||||
$this->decoder = Decoder::create();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(Manager $algorithmManager): self
|
||||
{
|
||||
return new self($algorithmManager);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'packed';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('sig', $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "sig" is missing.'
|
||||
);
|
||||
array_key_exists('alg', $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "alg" is missing.'
|
||||
);
|
||||
is_string($attestation['attStmt']['sig']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "sig" is missing.'
|
||||
);
|
||||
|
||||
return match (true) {
|
||||
array_key_exists('x5c', $attestation['attStmt']) => $this->loadBasicType($attestation),
|
||||
array_key_exists('ecdaaKeyId', $attestation['attStmt']) => $this->loadEcdaaType($attestation['attStmt']),
|
||||
default => $this->loadEmptyType($attestation),
|
||||
};
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
|
||||
return match (true) {
|
||||
$trustPath instanceof CertificateTrustPath => $this->processWithCertificate(
|
||||
$clientDataJSONHash,
|
||||
$attestationStatement,
|
||||
$authenticatorData,
|
||||
$trustPath
|
||||
),
|
||||
$trustPath instanceof EcdaaKeyIdTrustPath => $this->processWithECDAA(),
|
||||
$trustPath instanceof EmptyTrustPath => $this->processWithSelfAttestation(
|
||||
$clientDataJSONHash,
|
||||
$attestationStatement,
|
||||
$authenticatorData
|
||||
),
|
||||
default => throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Unsupported attestation statement'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
private function loadBasicType(array $attestation): AttestationStatement
|
||||
{
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
is_array($certificates) || throw AttestationStatementVerificationException::create(
|
||||
'The attestation statement value "x5c" must be a list with at least one certificate.'
|
||||
);
|
||||
count($certificates) > 0 || throw AttestationStatementVerificationException::create(
|
||||
'The attestation statement value "x5c" must be a list with at least one certificate.'
|
||||
);
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
|
||||
$attestationStatement = AttestationStatement::createBasic(
|
||||
$attestation['fmt'],
|
||||
$attestation['attStmt'],
|
||||
CertificateTrustPath::create($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
private function loadEcdaaType(array $attestation): AttestationStatement
|
||||
{
|
||||
$ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId'];
|
||||
is_string($ecdaaKeyId) || throw AttestationStatementVerificationException::create(
|
||||
'The attestation statement value "ecdaaKeyId" is invalid.'
|
||||
);
|
||||
|
||||
$attestationStatement = AttestationStatement::createEcdaa(
|
||||
$attestation['fmt'],
|
||||
$attestation['attStmt'],
|
||||
new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId'])
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
private function loadEmptyType(array $attestation): AttestationStatement
|
||||
{
|
||||
$attestationStatement = AttestationStatement::createSelf(
|
||||
$attestation['fmt'],
|
||||
$attestation['attStmt'],
|
||||
EmptyTrustPath::create()
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
|
||||
{
|
||||
$parsed = openssl_x509_parse($attestnCert);
|
||||
is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate');
|
||||
|
||||
//Check version
|
||||
isset($parsed['version']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate version'
|
||||
);
|
||||
$parsed['version'] === 2 || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate version'
|
||||
);
|
||||
|
||||
//Check subject field
|
||||
isset($parsed['name']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"'
|
||||
);
|
||||
str_contains(
|
||||
(string) $parsed['name'],
|
||||
'/OU=Authenticator Attestation'
|
||||
) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"'
|
||||
);
|
||||
|
||||
//Check extensions
|
||||
isset($parsed['extensions']) || throw AttestationStatementVerificationException::create(
|
||||
'Certificate extensions are missing'
|
||||
);
|
||||
is_array($parsed['extensions']) || throw AttestationStatementVerificationException::create(
|
||||
'Certificate extensions are missing'
|
||||
);
|
||||
|
||||
//Check certificate is not a CA cert
|
||||
isset($parsed['extensions']['basicConstraints']) || throw AttestationStatementVerificationException::create(
|
||||
'The Basic Constraints extension must have the CA component set to false'
|
||||
);
|
||||
$parsed['extensions']['basicConstraints'] === 'CA:FALSE' || throw AttestationStatementVerificationException::create(
|
||||
'The Basic Constraints extension must have the CA component set to false'
|
||||
);
|
||||
|
||||
$attestedCredentialData = $authenticatorData->attestedCredentialData;
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential available'
|
||||
);
|
||||
|
||||
// id-fido-gen-ce-aaguid OID check
|
||||
if (in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true)) {
|
||||
hash_equals(
|
||||
$attestedCredentialData->aaguid
|
||||
->toBinary(),
|
||||
$parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']
|
||||
) || throw AttestationStatementVerificationException::create(
|
||||
'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->certificates;
|
||||
|
||||
// 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->authData . $clientDataJSONHash;
|
||||
$result = openssl_verify(
|
||||
$signedData,
|
||||
$attestationStatement->get('sig'),
|
||||
$certificates[0],
|
||||
$opensslAlgorithmIdentifier
|
||||
);
|
||||
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
private function processWithECDAA(): never
|
||||
{
|
||||
throw UnsupportedFeatureException::create('ECDAA not supported');
|
||||
}
|
||||
|
||||
private function processWithSelfAttestation(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$attestedCredentialData = $authenticatorData->attestedCredentialData;
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential available'
|
||||
);
|
||||
$credentialPublicKey = $attestedCredentialData->credentialPublicKey;
|
||||
$credentialPublicKey !== null || throw AttestationStatementVerificationException::create(
|
||||
'No credential public key available'
|
||||
);
|
||||
$publicKeyStream = new StringStream($credentialPublicKey);
|
||||
$publicKey = $this->decoder->decode($publicKeyStream);
|
||||
$publicKeyStream->isEOF() || throw AttestationStatementVerificationException::create(
|
||||
'Invalid public key. Presence of extra bytes.'
|
||||
);
|
||||
$publicKeyStream->close();
|
||||
$publicKey instanceof MapObject || throw AttestationStatementVerificationException::create(
|
||||
'The attested credential data does not contain a valid public key.'
|
||||
);
|
||||
$publicKey = $publicKey->normalize();
|
||||
$publicKey = new Key($publicKey);
|
||||
$publicKey->alg() === (int) $attestationStatement->get(
|
||||
'alg'
|
||||
) || throw AttestationStatementVerificationException::create(
|
||||
'The algorithm of the attestation statement and the key are not identical.'
|
||||
);
|
||||
|
||||
$dataToVerify = $authenticatorData->authData . $clientDataJSONHash;
|
||||
$algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg'));
|
||||
if (! $algorithm instanceof Signature) {
|
||||
throw InvalidDataException::create($algorithm, 'Invalid algorithm');
|
||||
}
|
||||
$signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm);
|
||||
|
||||
return $algorithm->verify($dataToVerify, $publicKey, $signature);
|
||||
}
|
||||
}
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\OkpKey;
|
||||
use Cose\Key\RsaKey;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Lcobucci\Clock\Clock;
|
||||
use Lcobucci\Clock\SystemClock;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\Exception\UnsupportedFeatureException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function openssl_verify;
|
||||
use function unpack;
|
||||
|
||||
final class TPMAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private readonly Clock|ClockInterface $clock;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct(null|Clock|ClockInterface $clock = null)
|
||||
{
|
||||
if ($clock === null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/metadata-service',
|
||||
'4.5.0',
|
||||
'The parameter "$clock" will become mandatory in 5.0.0. Please set a valid PSR Clock implementation instead of "null".'
|
||||
);
|
||||
$clock = new SystemClock(new DateTimeZone('UTC'));
|
||||
}
|
||||
$this->clock = $clock;
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(null|Clock|ClockInterface $clock = null): self
|
||||
{
|
||||
return new self($clock);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'tpm';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
! array_key_exists(
|
||||
'ecdaaKeyId',
|
||||
$attestation['attStmt']
|
||||
) || throw AttestationStatementLoadingException::create($attestation, 'ECDAA not supported');
|
||||
foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
|
||||
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
sprintf('The attestation statement value "%s" is missing.', $key)
|
||||
);
|
||||
}
|
||||
$attestation['attStmt']['ver'] === '2.0' || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
|
||||
$certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
|
||||
bin2hex((string) $certInfo['type']) === '8017' || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
|
||||
$pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
|
||||
$pubAreaAttestedNameAlg = mb_substr((string) $certInfo['attestedName'], 0, 2, '8bit');
|
||||
$pubAreaHash = hash(
|
||||
$this->getTPMHash($pubAreaAttestedNameAlg),
|
||||
(string) $attestation['attStmt']['pubArea'],
|
||||
true
|
||||
);
|
||||
$attestedName = $pubAreaAttestedNameAlg . $pubAreaHash;
|
||||
$attestedName === $certInfo['attestedName'] || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attested name'
|
||||
);
|
||||
|
||||
$attestation['attStmt']['parsedCertInfo'] = $certInfo;
|
||||
$attestation['attStmt']['parsedPubArea'] = $pubArea;
|
||||
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
|
||||
count($certificates) > 0 || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'The attestation statement value "x5c" must be a list with at least one certificate.'
|
||||
);
|
||||
|
||||
$attestationStatement = AttestationStatement::createAttCA(
|
||||
$this->name(),
|
||||
$attestation['attStmt'],
|
||||
CertificateTrustPath::create($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$attToBeSigned = $authenticatorData->authData . $clientDataJSONHash;
|
||||
$attToBeSignedHash = hash(
|
||||
Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')),
|
||||
$attToBeSigned,
|
||||
true
|
||||
);
|
||||
$attestationStatement->get(
|
||||
'parsedCertInfo'
|
||||
)['extraData'] === $attToBeSignedHash || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid attestation hash'
|
||||
);
|
||||
$credentialPublicKey = $authenticatorData->attestedCredentialData?->credentialPublicKey;
|
||||
$credentialPublicKey !== null || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Not credential public key available in the attested credential data'
|
||||
);
|
||||
$this->checkUniquePublicKey($attestationStatement->get('parsedPubArea')['unique'], $credentialPublicKey);
|
||||
|
||||
return match (true) {
|
||||
$attestationStatement->trustPath instanceof CertificateTrustPath => $this->processWithCertificate(
|
||||
$attestationStatement,
|
||||
$authenticatorData
|
||||
),
|
||||
$attestationStatement->trustPath instanceof EcdaaKeyIdTrustPath => $this->processWithECDAA(),
|
||||
default => throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Unsupported attestation statement'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
|
||||
{
|
||||
$cborDecoder = Decoder::create();
|
||||
$publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
|
||||
$publicKey instanceof MapObject || throw AttestationStatementVerificationException::create(
|
||||
'Invalid public key'
|
||||
);
|
||||
$key = Key::create($publicKey->normalize());
|
||||
|
||||
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 AttestationStatementVerificationException::create('Invalid or unsupported key type.');
|
||||
}
|
||||
|
||||
$unique === $uniqueFromKey || throw AttestationStatementVerificationException::create(
|
||||
'Invalid pubArea.unique value'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function checkCertInfo(string $data): array
|
||||
{
|
||||
$certInfo = new StringStream($data);
|
||||
|
||||
$magic = $certInfo->read(4);
|
||||
bin2hex($magic) === 'ff544347' || throw AttestationStatementVerificationException::create(
|
||||
'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
|
||||
$certInfo->isEOF() || throw AttestationStatementVerificationException::create(
|
||||
'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);
|
||||
|
||||
$unique = $this->getUnique($type, $pubArea);
|
||||
$pubArea->isEOF() || throw AttestationStatementVerificationException::create(
|
||||
'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
|
||||
{
|
||||
return match (bin2hex($type)) {
|
||||
'0001' => [
|
||||
'symmetric' => $stream->read(2),
|
||||
'scheme' => $stream->read(2),
|
||||
'keyBits' => unpack('n', $stream->read(2))[1],
|
||||
'exponent' => $this->getExponent($stream->read(4)),
|
||||
],
|
||||
'0023' => [
|
||||
'symmetric' => $stream->read(2),
|
||||
'scheme' => $stream->read(2),
|
||||
'curveId' => $stream->read(2),
|
||||
'kdf' => $stream->read(2),
|
||||
],
|
||||
default => throw AttestationStatementVerificationException::create('Unsupported type'),
|
||||
};
|
||||
}
|
||||
|
||||
private function getUnique(string $type, StringStream $stream): string
|
||||
{
|
||||
switch (bin2hex($type)) {
|
||||
case '0001':
|
||||
$uniqueLength = unpack('n', $stream->read(2))[1];
|
||||
return $stream->read($uniqueLength);
|
||||
case '0023':
|
||||
$xLen = unpack('n', $stream->read(2))[1];
|
||||
$x = $stream->read($xLen);
|
||||
$yLen = unpack('n', $stream->read(2))[1];
|
||||
$y = $stream->read($yLen);
|
||||
return "\04" . $x . $y;
|
||||
default:
|
||||
throw AttestationStatementVerificationException::create('Unsupported type');
|
||||
}
|
||||
}
|
||||
|
||||
private function getExponent(string $exponent): string
|
||||
{
|
||||
return bin2hex($exponent) === '00000000' ? Base64UrlSafe::decodeNoPadding('AQAB') : $exponent;
|
||||
}
|
||||
|
||||
private function getTPMHash(string $nameAlg): string
|
||||
{
|
||||
return match (bin2hex($nameAlg)) {
|
||||
'0004' => 'sha1',
|
||||
'000b' => 'sha256',
|
||||
'000c' => 'sha384',
|
||||
'000d' => 'sha512',
|
||||
default => throw AttestationStatementVerificationException::create('Unsupported hash algorithm'),
|
||||
};
|
||||
}
|
||||
|
||||
private function processWithCertificate(
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
$trustPath instanceof CertificateTrustPath || throw AttestationStatementVerificationException::create(
|
||||
'Invalid trust path'
|
||||
);
|
||||
|
||||
$certificates = $trustPath->certificates;
|
||||
|
||||
// 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 $result === 1;
|
||||
}
|
||||
|
||||
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
|
||||
{
|
||||
$parsed = openssl_x509_parse($attestnCert);
|
||||
is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate');
|
||||
|
||||
//Check version
|
||||
(isset($parsed['version']) && $parsed['version'] === 2) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate version'
|
||||
);
|
||||
|
||||
//Check subject field is empty
|
||||
isset($parsed['subject']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate name. The Subject should be empty'
|
||||
);
|
||||
is_array($parsed['subject']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate name. The Subject should be empty'
|
||||
);
|
||||
count($parsed['subject']) === 0 || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate name. The Subject should be empty'
|
||||
);
|
||||
|
||||
// Check period of validity
|
||||
array_key_exists(
|
||||
'validFrom_time_t',
|
||||
$parsed
|
||||
) || throw AttestationStatementVerificationException::create('Invalid certificate start date.');
|
||||
is_int($parsed['validFrom_time_t']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate start date.'
|
||||
);
|
||||
$startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
|
||||
$startDate < $this->clock->now() || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate start date.'
|
||||
);
|
||||
|
||||
array_key_exists('validTo_time_t', $parsed) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate end date.'
|
||||
);
|
||||
is_int($parsed['validTo_time_t']) || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate end date.'
|
||||
);
|
||||
$endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
|
||||
$endDate > $this->clock->now() || throw AttestationStatementVerificationException::create(
|
||||
'Invalid certificate end date.'
|
||||
);
|
||||
|
||||
//Check extensions
|
||||
(isset($parsed['extensions']) && is_array(
|
||||
$parsed['extensions']
|
||||
)) || throw AttestationStatementVerificationException::create('Certificate extensions are missing');
|
||||
|
||||
//Check subjectAltName
|
||||
isset($parsed['extensions']['subjectAltName']) || throw AttestationStatementVerificationException::create(
|
||||
'The "subjectAltName" is missing'
|
||||
);
|
||||
|
||||
//Check extendedKeyUsage
|
||||
isset($parsed['extensions']['extendedKeyUsage']) || throw AttestationStatementVerificationException::create(
|
||||
'The "subjectAltName" is missing'
|
||||
);
|
||||
$parsed['extensions']['extendedKeyUsage'] === '2.23.133.8.3' || throw AttestationStatementVerificationException::create(
|
||||
'The "extendedKeyUsage" is invalid'
|
||||
);
|
||||
|
||||
// id-fido-gen-ce-aaguid OID check
|
||||
in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && ! hash_equals(
|
||||
$authenticatorData->attestedCredentialData
|
||||
?->aaguid
|
||||
->toBinary() ?? '',
|
||||
$parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']
|
||||
) && throw AttestationStatementVerificationException::create(
|
||||
'The value of the "aaguid" does not match with the certificate'
|
||||
);
|
||||
}
|
||||
|
||||
private function processWithECDAA(): never
|
||||
{
|
||||
throw UnsupportedFeatureException::create('ECDAA not supported');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use JsonSerializable;
|
||||
use ParagonIE\ConstantTime\Base64;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use function array_key_exists;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#sec-attested-credential-data
|
||||
*/
|
||||
class AttestedCredentialData implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public Uuid $aaguid,
|
||||
public readonly string $credentialId,
|
||||
public readonly ?string $credentialPublicKey
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(Uuid $aaguid, string $credentialId, ?string $credentialPublicKey = null): self
|
||||
{
|
||||
return new self($aaguid, $credentialId, $credentialPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAaguid(): Uuid
|
||||
{
|
||||
return $this->aaguid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setAaguid(Uuid $aaguid): void
|
||||
{
|
||||
$this->aaguid = $aaguid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getCredentialId(): string
|
||||
{
|
||||
return $this->credentialId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getCredentialPublicKey(): ?string
|
||||
{
|
||||
return $this->credentialPublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
* @deprecated since 4.9.0 and will be removed in 5.0.0. Please use the serializer instead.
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
array_key_exists('aaguid', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "aaguid" is missing.'
|
||||
);
|
||||
$aaguid = $json['aaguid'];
|
||||
is_string($aaguid) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "aaguid" shall be a string of 36 characters'
|
||||
);
|
||||
mb_strlen($aaguid, '8bit') === 36 || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "aaguid" shall be a string of 36 characters'
|
||||
);
|
||||
$uuid = Uuid::fromString($aaguid);
|
||||
|
||||
array_key_exists('credentialId', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "credentialId" is missing.'
|
||||
);
|
||||
$credentialId = $json['credentialId'];
|
||||
is_string($credentialId) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "credentialId" shall be a string'
|
||||
);
|
||||
$credentialId = Base64::decode($credentialId, true);
|
||||
|
||||
$credentialPublicKey = null;
|
||||
if (isset($json['credentialPublicKey'])) {
|
||||
$credentialPublicKey = Base64::decode($json['credentialPublicKey'], true);
|
||||
}
|
||||
|
||||
return self::create($uuid, $credentialId, $credentialPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-bundle',
|
||||
'4.9.0',
|
||||
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
|
||||
__METHOD__
|
||||
);
|
||||
$result = [
|
||||
'aaguid' => $this->aaguid->__toString(),
|
||||
'credentialId' => base64_encode($this->credentialId),
|
||||
];
|
||||
if ($this->credentialPublicKey !== null) {
|
||||
$result['credentialPublicKey'] = base64_encode($this->credentialPublicKey);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class AuthenticationExtension implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly mixed $value
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(string $name, mixed $value): self
|
||||
{
|
||||
return new self($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function value(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-bundle',
|
||||
'4.9.0',
|
||||
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
|
||||
__METHOD__
|
||||
);
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use ArrayAccess;
|
||||
use ArrayIterator;
|
||||
use Countable;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\AuthenticationExtensionException;
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_string;
|
||||
use const COUNT_NORMAL;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<AuthenticationExtension>
|
||||
* @final
|
||||
*/
|
||||
class AuthenticationExtensions implements JsonSerializable, Countable, IteratorAggregate, ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var array<string, AuthenticationExtension>
|
||||
* @readonly
|
||||
*/
|
||||
public array $extensions;
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed|AuthenticationExtension> $extensions
|
||||
*/
|
||||
public function __construct(array $extensions = [])
|
||||
{
|
||||
$list = [];
|
||||
foreach ($extensions as $key => $extension) {
|
||||
if ($extension instanceof AuthenticationExtension) {
|
||||
$list[$extension->name] = $extension;
|
||||
|
||||
continue;
|
||||
}
|
||||
if (is_string($key)) {
|
||||
$list[$key] = AuthenticationExtension::create($key, $extension);
|
||||
continue;
|
||||
}
|
||||
throw new AuthenticationExtensionException('Invalid extension');
|
||||
}
|
||||
$this->extensions = $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, AuthenticationExtension> $extensions
|
||||
*/
|
||||
public static function create(array $extensions = []): static
|
||||
{
|
||||
return new static($extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function add(AuthenticationExtension ...$extensions): static
|
||||
{
|
||||
foreach ($extensions as $extension) {
|
||||
$this->extensions[$extension->name] = $extension;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public static function createFromArray(array $json): static
|
||||
{
|
||||
return static::create(
|
||||
array_map(
|
||||
static fn (string $key, mixed $value): AuthenticationExtension => AuthenticationExtension::create(
|
||||
$key,
|
||||
$value
|
||||
),
|
||||
array_keys($json),
|
||||
$json
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->extensions);
|
||||
}
|
||||
|
||||
public function get(string $key): AuthenticationExtension
|
||||
{
|
||||
$this->has($key) || throw AuthenticationExtensionException::create(sprintf(
|
||||
'The extension with key "%s" is not available',
|
||||
$key
|
||||
));
|
||||
|
||||
return $this->extensions[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, AuthenticationExtension>
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-bundle',
|
||||
'4.9.0',
|
||||
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
|
||||
__METHOD__
|
||||
);
|
||||
return $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);
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return array_key_exists($offset, $this->extensions);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->extensions[$offset];
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if ($value === null) {
|
||||
return;
|
||||
}
|
||||
if ($value instanceof AuthenticationExtension) {
|
||||
$this->extensions[$value->name] = $value;
|
||||
return;
|
||||
}
|
||||
if (is_string($offset)) {
|
||||
$this->extensions[$offset] = AuthenticationExtension::create($offset, $value);
|
||||
return;
|
||||
}
|
||||
throw new AuthenticationExtensionException('Invalid extension');
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
unset($this->extensions[$offset]);
|
||||
}
|
||||
}
|
||||
3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
/**
|
||||
* @deprecated since 4.8.0. Use {Webauthn\AuthenticationExtensions\AuthenticationExtensions} instead.
|
||||
*/
|
||||
class AuthenticationExtensionsClientInputs extends AuthenticationExtensions
|
||||
{
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
/**
|
||||
* @deprecated since 4.8.0. Use {Webauthn\AuthenticationExtensions\AuthenticationExtensions} instead.
|
||||
*/
|
||||
class AuthenticationExtensionsClientOutputs extends AuthenticationExtensions
|
||||
{
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use CBOR\CBORObject;
|
||||
use CBOR\MapObject;
|
||||
use Webauthn\Exception\AuthenticationExtensionException;
|
||||
|
||||
abstract class AuthenticationExtensionsClientOutputsLoader
|
||||
{
|
||||
public static function load(CBORObject $object): AuthenticationExtensions
|
||||
{
|
||||
$object instanceof MapObject || throw AuthenticationExtensionException::create('Invalid extension object');
|
||||
$data = $object->normalize();
|
||||
return AuthenticationExtensionsClientOutputs::create(
|
||||
array_map(
|
||||
fn (mixed $value, string $key) => AuthenticationExtension::create($key, $value),
|
||||
$data,
|
||||
array_keys($data)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
interface ExtensionOutputChecker
|
||||
{
|
||||
public function check(AuthenticationExtensions $inputs, AuthenticationExtensions $outputs): void;
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
class ExtensionOutputCheckerHandler
|
||||
{
|
||||
/**
|
||||
* @var ExtensionOutputChecker[]
|
||||
*/
|
||||
private array $checkers = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function add(ExtensionOutputChecker $checker): void
|
||||
{
|
||||
$this->checkers[] = $checker;
|
||||
}
|
||||
|
||||
public function check(AuthenticationExtensions $inputs, AuthenticationExtensions $outputs): void
|
||||
{
|
||||
foreach ($this->checkers as $checker) {
|
||||
$checker->check($inputs, $outputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class ExtensionOutputError extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
public readonly AuthenticationExtension $authenticationExtension,
|
||||
string $message = '',
|
||||
int $code = 0,
|
||||
Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAuthenticationExtension(): AuthenticationExtension
|
||||
{
|
||||
return $this->authenticationExtension;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationObject;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
|
||||
*/
|
||||
class AuthenticatorAssertionResponse extends AuthenticatorResponse
|
||||
{
|
||||
public function __construct(
|
||||
CollectedClientData $clientDataJSON,
|
||||
public readonly AuthenticatorData $authenticatorData,
|
||||
public readonly string $signature,
|
||||
public readonly ?string $userHandle,
|
||||
public readonly null|AttestationObject $attestationObject = null,
|
||||
) {
|
||||
parent::__construct($clientDataJSON);
|
||||
}
|
||||
|
||||
public static function create(
|
||||
CollectedClientData $clientDataJSON,
|
||||
AuthenticatorData $authenticatorData,
|
||||
string $signature,
|
||||
?string $userHandle = null,
|
||||
null|AttestationObject $attestationObject = null,
|
||||
): self {
|
||||
return new self($clientDataJSON, $authenticatorData, $signature, $userHandle, $attestationObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getSignature(): string
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getUserHandle(): ?string
|
||||
{
|
||||
return $this->userHandle;
|
||||
}
|
||||
}
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Cose\Algorithm\Manager;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\CeremonyStep\CeremonyStepManager;
|
||||
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
|
||||
use Webauthn\Counter\CounterChecker;
|
||||
use Webauthn\Event\AuthenticatorAssertionResponseValidationFailedEvent;
|
||||
use Webauthn\Event\AuthenticatorAssertionResponseValidationSucceededEvent;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\TokenBinding\TokenBindingHandler;
|
||||
use function is_string;
|
||||
|
||||
class AuthenticatorAssertionResponseValidator implements CanLogData, CanDispatchEvents
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private readonly CeremonyStepManagerFactory $ceremonyStepManagerFactory;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
|
||||
private readonly null|TokenBindingHandler $tokenBindingHandler = null,
|
||||
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
|
||||
null|Manager $algorithmManager = null,
|
||||
null|EventDispatcherInterface $eventDispatcher = null,
|
||||
private null|CeremonyStepManager $ceremonyStepManager = null
|
||||
) {
|
||||
if ($this->publicKeyCredentialSourceRepository !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.6.0',
|
||||
'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.'
|
||||
);
|
||||
}
|
||||
if ($this->tokenBindingHandler !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.3.0',
|
||||
'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.'
|
||||
);
|
||||
}
|
||||
if ($extensionOutputCheckerHandler !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.'
|
||||
);
|
||||
}
|
||||
if ($algorithmManager !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$algorithmManager" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckSignature object into the CeremonyStepManager.'
|
||||
);
|
||||
}
|
||||
$this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher();
|
||||
if ($eventDispatcher !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.'
|
||||
);
|
||||
}
|
||||
if ($this->ceremonyStepManager === null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $algorithmManager and $extensionOutputCheckerHandler.'
|
||||
);
|
||||
}
|
||||
$this->logger = new NullLogger();
|
||||
|
||||
$this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
|
||||
if ($extensionOutputCheckerHandler !== null) {
|
||||
$this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler);
|
||||
}
|
||||
if ($algorithmManager !== null) {
|
||||
$this->ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager);
|
||||
}
|
||||
}
|
||||
|
||||
public static function create(
|
||||
null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
|
||||
null|TokenBindingHandler $tokenBindingHandler = null,
|
||||
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
|
||||
null|Manager $algorithmManager = null,
|
||||
null|EventDispatcherInterface $eventDispatcher = null,
|
||||
null|CeremonyStepManager $ceremonyStepManager = null
|
||||
): self {
|
||||
return new self(
|
||||
$publicKeyCredentialSourceRepository,
|
||||
$tokenBindingHandler,
|
||||
$extensionOutputCheckerHandler,
|
||||
$algorithmManager,
|
||||
$eventDispatcher,
|
||||
$ceremonyStepManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $securedRelyingPartyId
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||
*/
|
||||
public function check(
|
||||
string|PublicKeyCredentialSource $credentialId,
|
||||
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
?string $userHandle,
|
||||
null|array $securedRelyingPartyId = null
|
||||
): PublicKeyCredentialSource {
|
||||
if ($request instanceof ServerRequestInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
sprintf(
|
||||
'Passing a %s to the method `check` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
|
||||
ServerRequestInterface::class,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if (is_string($credentialId)) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.6.0',
|
||||
sprintf(
|
||||
'Passing a string as first to the method `check` of the class "%s" is deprecated since 4.6.0. Please inject a %s object instead.',
|
||||
self::class,
|
||||
PublicKeyCredentialSource::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($securedRelyingPartyId !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
sprintf(
|
||||
'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckOrigin into the CeremonyStepManager instead.',
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ($credentialId instanceof PublicKeyCredentialSource) {
|
||||
$publicKeyCredentialSource = $credentialId;
|
||||
} else {
|
||||
$this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository || throw AuthenticatorResponseVerificationException::create(
|
||||
'Please pass the Public Key Credential Source to the method "check".'
|
||||
);
|
||||
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId(
|
||||
$credentialId
|
||||
);
|
||||
}
|
||||
$publicKeyCredentialSource !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'The credential ID is invalid.'
|
||||
);
|
||||
$host = is_string($request) ? $request : $request->getUri()
|
||||
->getHost();
|
||||
|
||||
if ($this->ceremonyStepManager === null) {
|
||||
$this->ceremonyStepManager = $this->ceremonyStepManagerFactory->requestCeremony($securedRelyingPartyId);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->logger->info('Checking the authenticator assertion response', [
|
||||
'credentialId' => $credentialId,
|
||||
'publicKeyCredentialSource' => $publicKeyCredentialSource,
|
||||
'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
|
||||
'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
|
||||
'host' => $host,
|
||||
'userHandle' => $userHandle,
|
||||
]);
|
||||
|
||||
$this->ceremonyStepManager->process(
|
||||
$publicKeyCredentialSource,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$userHandle,
|
||||
$host
|
||||
);
|
||||
|
||||
$publicKeyCredentialSource->counter = $authenticatorAssertionResponse->authenticatorData->signCount; //26.1.
|
||||
$publicKeyCredentialSource->backupEligible = $authenticatorAssertionResponse->authenticatorData->isBackupEligible(); //26.2.
|
||||
$publicKeyCredentialSource->backupStatus = $authenticatorAssertionResponse->authenticatorData->isBackedUp(); //26.2.
|
||||
if ($publicKeyCredentialSource->uvInitialized === false) {
|
||||
$publicKeyCredentialSource->uvInitialized = $authenticatorAssertionResponse->authenticatorData->isUserVerified(); //26.3.
|
||||
}
|
||||
/*
|
||||
* 26.3.
|
||||
* OPTIONALLY, if response.attestationObject is present, update credentialRecord.attestationObject to the value of response.attestationObject and update credentialRecord.attestationClientDataJSON to the value of response.clientDataJSON.
|
||||
*/
|
||||
|
||||
if (is_string(
|
||||
$credentialId
|
||||
) && ($this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository)) {
|
||||
$this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
|
||||
}
|
||||
//All good. We can continue.
|
||||
$this->logger->info('The assertion is valid');
|
||||
$this->logger->debug('Public Key Credential Source', [
|
||||
'publicKeyCredentialSource' => $publicKeyCredentialSource,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAssertionResponseValidationSucceededEvent(
|
||||
null,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$host,
|
||||
$userHandle,
|
||||
$publicKeyCredentialSource
|
||||
)
|
||||
);
|
||||
// 27.
|
||||
return $publicKeyCredentialSource;
|
||||
} catch (AuthenticatorResponseVerificationException $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAssertionResponseValidationFailedEvent(
|
||||
$publicKeyCredentialSource,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$host,
|
||||
$userHandle,
|
||||
$throwable
|
||||
)
|
||||
);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckCounter object into a CeremonyStepManager instead.
|
||||
*/
|
||||
public function setCounterChecker(CounterChecker $counterChecker): self
|
||||
{
|
||||
$this->ceremonyStepManagerFactory->setCounterChecker($counterChecker);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAssertionResponseValidationSucceededEvent(
|
||||
null|string $credentialId,
|
||||
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface|string $host,
|
||||
?string $userHandle,
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource
|
||||
): AuthenticatorAssertionResponseValidationSucceededEvent {
|
||||
if ($host instanceof ServerRequestInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
sprintf(
|
||||
'Passing a %s to the method `createAuthenticatorAssertionResponseValidationSucceededEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
|
||||
ServerRequestInterface::class,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
return new AuthenticatorAssertionResponseValidationSucceededEvent(
|
||||
$credentialId,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$host,
|
||||
$userHandle,
|
||||
$publicKeyCredentialSource
|
||||
);
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAssertionResponseValidationFailedEvent(
|
||||
string|PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface|string $host,
|
||||
?string $userHandle,
|
||||
Throwable $throwable
|
||||
): AuthenticatorAssertionResponseValidationFailedEvent {
|
||||
if ($host instanceof ServerRequestInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
sprintf(
|
||||
'Passing a %s to the method `createAuthenticatorAssertionResponseValidationFailedEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
|
||||
ServerRequestInterface::class,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
return new AuthenticatorAssertionResponseValidationFailedEvent(
|
||||
$publicKeyCredentialSource,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$host,
|
||||
$userHandle,
|
||||
$throwable
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationObject;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
|
||||
*/
|
||||
class AuthenticatorAttestationResponse extends AuthenticatorResponse
|
||||
{
|
||||
/**
|
||||
* @param string[] $transports
|
||||
*/
|
||||
public function __construct(
|
||||
CollectedClientData $clientDataJSON,
|
||||
public readonly AttestationObject $attestationObject,
|
||||
public readonly array $transports = []
|
||||
) {
|
||||
parent::__construct($clientDataJSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $transports
|
||||
*/
|
||||
public static function create(
|
||||
CollectedClientData $clientDataJSON,
|
||||
AttestationObject $attestationObject,
|
||||
array $transports = []
|
||||
): self {
|
||||
return new self($clientDataJSON, $attestationObject, $transports);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAttestationObject(): AttestationObject
|
||||
{
|
||||
return $this->attestationObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTransports(): array
|
||||
{
|
||||
return $this->transports;
|
||||
}
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Throwable;
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\CeremonyStep\CeremonyStepManager;
|
||||
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
|
||||
use Webauthn\Event\AuthenticatorAttestationResponseValidationFailedEvent;
|
||||
use Webauthn\Event\AuthenticatorAttestationResponseValidationSucceededEvent;
|
||||
use Webauthn\Event\CanDispatchEvents;
|
||||
use Webauthn\Event\NullEventDispatcher;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
|
||||
use Webauthn\MetadataService\MetadataStatementRepository;
|
||||
use Webauthn\MetadataService\StatusReportRepository;
|
||||
use Webauthn\TokenBinding\TokenBindingHandler;
|
||||
use function is_string;
|
||||
|
||||
class AuthenticatorAttestationResponseValidator implements CanLogData, CanDispatchEvents
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
private readonly CeremonyStepManagerFactory $ceremonyStepManagerFactory;
|
||||
|
||||
public function __construct(
|
||||
null|AttestationStatementSupportManager $attestationStatementSupportManager = null,
|
||||
private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
|
||||
private readonly null|TokenBindingHandler $tokenBindingHandler = null,
|
||||
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
|
||||
null|EventDispatcherInterface $eventDispatcher = null,
|
||||
private null|CeremonyStepManager $ceremonyStepManager = null
|
||||
) {
|
||||
if ($this->publicKeyCredentialSourceRepository !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.6.0',
|
||||
'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.'
|
||||
);
|
||||
}
|
||||
if ($this->tokenBindingHandler !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.3.0',
|
||||
'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.'
|
||||
);
|
||||
}
|
||||
if ($extensionOutputCheckerHandler !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.'
|
||||
);
|
||||
}
|
||||
$this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher();
|
||||
if ($eventDispatcher !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.'
|
||||
);
|
||||
}
|
||||
if ($this->ceremonyStepManager === null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $attestationStatementSupportManager and $extensionOutputCheckerHandler.'
|
||||
);
|
||||
}
|
||||
$this->logger = new NullLogger();
|
||||
$this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
|
||||
if ($attestationStatementSupportManager !== null) {
|
||||
$this->ceremonyStepManagerFactory->setAttestationStatementSupportManager(
|
||||
$attestationStatementSupportManager
|
||||
);
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
'The parameter "$attestationStatementSupportManager" is deprecated since 4.8.0 will be removed in 5.0.0. Please set a CheckAttestationFormatIsKnownAndValid object into CeremonyStepManager object instead.'
|
||||
);
|
||||
}
|
||||
if ($extensionOutputCheckerHandler !== null) {
|
||||
$this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private Will become private in 5.0.0
|
||||
*/
|
||||
public static function create(
|
||||
null|AttestationStatementSupportManager $attestationStatementSupportManager = null,
|
||||
null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
|
||||
null|TokenBindingHandler $tokenBindingHandler = null,
|
||||
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
|
||||
null|EventDispatcherInterface $eventDispatcher = null,
|
||||
null|CeremonyStepManager $ceremonyStepManager = null,
|
||||
): self {
|
||||
return new self(
|
||||
$attestationStatementSupportManager,
|
||||
$publicKeyCredentialSourceRepository,
|
||||
$tokenBindingHandler,
|
||||
$extensionOutputCheckerHandler,
|
||||
$eventDispatcher,
|
||||
$ceremonyStepManager
|
||||
);
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead.
|
||||
*/
|
||||
public function setCertificateChainValidator(CertificateChainValidator $certificateChainValidator): self
|
||||
{
|
||||
$this->ceremonyStepManagerFactory->enableCertificateChainValidator($certificateChainValidator);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead.
|
||||
*/
|
||||
public function enableMetadataStatementSupport(
|
||||
MetadataStatementRepository $metadataStatementRepository,
|
||||
StatusReportRepository $statusReportRepository,
|
||||
CertificateChainValidator $certificateChainValidator
|
||||
): self {
|
||||
$this->ceremonyStepManagerFactory->enableMetadataStatementSupport(
|
||||
$metadataStatementRepository,
|
||||
$statusReportRepository,
|
||||
$certificateChainValidator
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $securedRelyingPartyId
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||
*/
|
||||
public function check(
|
||||
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
null|array $securedRelyingPartyId = null,
|
||||
): PublicKeyCredentialSource {
|
||||
if ($request instanceof ServerRequestInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
sprintf(
|
||||
'Passing a %s to the method `check` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
|
||||
ServerRequestInterface::class,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
if ($securedRelyingPartyId !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.8.0',
|
||||
sprintf(
|
||||
'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject the list instead.',
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
$host = is_string($request) ? $request : $request->getUri()
|
||||
->getHost();
|
||||
try {
|
||||
$this->logger->info('Checking the authenticator attestation response', [
|
||||
'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
|
||||
'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
|
||||
'host' => $host,
|
||||
]);
|
||||
if ($this->ceremonyStepManager === null) {
|
||||
$this->ceremonyStepManager = $this->ceremonyStepManagerFactory->creationCeremony(
|
||||
$securedRelyingPartyId
|
||||
);
|
||||
}
|
||||
|
||||
$publicKeyCredentialSource = $this->createPublicKeyCredentialSource(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions
|
||||
);
|
||||
|
||||
$this->ceremonyStepManager->process(
|
||||
$publicKeyCredentialSource,
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$publicKeyCredentialCreationOptions->user->id,
|
||||
$host
|
||||
);
|
||||
|
||||
$publicKeyCredentialSource->counter = $authenticatorAttestationResponse->attestationObject->authData->signCount;
|
||||
$publicKeyCredentialSource->backupEligible = $authenticatorAttestationResponse->attestationObject->authData->isBackupEligible();
|
||||
$publicKeyCredentialSource->backupStatus = $authenticatorAttestationResponse->attestationObject->authData->isBackedUp();
|
||||
$publicKeyCredentialSource->uvInitialized = $authenticatorAttestationResponse->attestationObject->authData->isUserVerified();
|
||||
|
||||
$this->logger->info('The attestation is valid');
|
||||
$this->logger->debug('Public Key Credential Source', [
|
||||
'publicKeyCredentialSource' => $publicKeyCredentialSource,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAttestationResponseValidationSucceededEvent(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$host,
|
||||
$publicKeyCredentialSource
|
||||
)
|
||||
);
|
||||
return $publicKeyCredentialSource;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAttestationResponseValidationFailedEvent(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$host,
|
||||
$throwable
|
||||
)
|
||||
);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAttestationResponseValidationSucceededEvent(
|
||||
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
ServerRequestInterface|string $host,
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource
|
||||
): AuthenticatorAttestationResponseValidationSucceededEvent {
|
||||
if ($host instanceof ServerRequestInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
sprintf(
|
||||
'Passing a %s to the method `createAuthenticatorAttestationResponseValidationSucceededEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
|
||||
ServerRequestInterface::class,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
return new AuthenticatorAttestationResponseValidationSucceededEvent(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$host,
|
||||
$publicKeyCredentialSource
|
||||
);
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAttestationResponseValidationFailedEvent(
|
||||
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
ServerRequestInterface|string $host,
|
||||
Throwable $throwable
|
||||
): AuthenticatorAttestationResponseValidationFailedEvent {
|
||||
if ($host instanceof ServerRequestInterface) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-lib',
|
||||
'4.5.0',
|
||||
sprintf(
|
||||
'Passing a %s to the method `createAuthenticatorAttestationResponseValidationFailedEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
|
||||
ServerRequestInterface::class,
|
||||
self::class
|
||||
)
|
||||
);
|
||||
}
|
||||
return new AuthenticatorAttestationResponseValidationFailedEvent(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$host,
|
||||
$throwable
|
||||
);
|
||||
}
|
||||
|
||||
private function createPublicKeyCredentialSource(
|
||||
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
): PublicKeyCredentialSource {
|
||||
$attestationObject = $authenticatorAttestationResponse->attestationObject;
|
||||
$attestedCredentialData = $attestationObject->authData->attestedCredentialData;
|
||||
$attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'Not attested credential data'
|
||||
);
|
||||
$credentialId = $attestedCredentialData->credentialId;
|
||||
$credentialPublicKey = $attestedCredentialData->credentialPublicKey;
|
||||
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'Not credential public key available in the attested credential data'
|
||||
);
|
||||
$userHandle = $publicKeyCredentialCreationOptions->user->id;
|
||||
$transports = $authenticatorAttestationResponse->transports;
|
||||
|
||||
return PublicKeyCredentialSource::create(
|
||||
$credentialId,
|
||||
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
|
||||
$transports,
|
||||
$attestationObject->attStmt
|
||||
->type,
|
||||
$attestationObject->attStmt
|
||||
->trustPath,
|
||||
$attestedCredentialData->aaguid,
|
||||
$credentialPublicKey,
|
||||
$userHandle,
|
||||
$attestationObject->authData
|
||||
->signCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
|
||||
use function ord;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#sec-authenticator-data
|
||||
* @see https://www.w3.org/TR/webauthn/#flags
|
||||
*/
|
||||
class AuthenticatorData
|
||||
{
|
||||
final public const FLAG_UP = 0b00000001;
|
||||
|
||||
final public const FLAG_RFU1 = 0b00000010;
|
||||
|
||||
final public const FLAG_UV = 0b00000100;
|
||||
|
||||
final public const FLAG_BE = 0b00001000;
|
||||
|
||||
final public const FLAG_BS = 0b00010000;
|
||||
|
||||
/**
|
||||
* TODO: remove bits 3 and 4 as they have been assigned to BE and BS in Webauthn level 3.
|
||||
*/
|
||||
final public const FLAG_RFU2 = 0b00111000;
|
||||
|
||||
final public const FLAG_AT = 0b01000000;
|
||||
|
||||
final public const FLAG_ED = 0b10000000;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $authData,
|
||||
public readonly string $rpIdHash,
|
||||
public readonly string $flags,
|
||||
public readonly int $signCount,
|
||||
public readonly null|AttestedCredentialData $attestedCredentialData,
|
||||
public readonly null|AuthenticationExtensions $extensions
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(
|
||||
string $authData,
|
||||
string $rpIdHash,
|
||||
string $flags,
|
||||
int $signCount,
|
||||
null|AttestedCredentialData $attestedCredentialData = null,
|
||||
null|AuthenticationExtensions $extensions = null
|
||||
): self {
|
||||
return new self($authData, $rpIdHash, $flags, $signCount, $attestedCredentialData, $extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAuthData(): string
|
||||
{
|
||||
return $this->authData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getRpIdHash(): string
|
||||
{
|
||||
return $this->rpIdHash;
|
||||
}
|
||||
|
||||
public function isUserPresent(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_UP);
|
||||
}
|
||||
|
||||
public function isUserVerified(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_UV);
|
||||
}
|
||||
|
||||
public function isBackupEligible(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_BE);
|
||||
}
|
||||
|
||||
public function isBackedUp(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_BS);
|
||||
}
|
||||
|
||||
public function hasAttestedCredentialData(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_AT);
|
||||
}
|
||||
|
||||
public function hasExtensions(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_ED);
|
||||
}
|
||||
|
||||
public function getReservedForFutureUse1(): int
|
||||
{
|
||||
return ord($this->flags) & self::FLAG_RFU1;
|
||||
}
|
||||
|
||||
public function getReservedForFutureUse2(): int
|
||||
{
|
||||
return ord($this->flags) & self::FLAG_RFU2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getSignCount(): int
|
||||
{
|
||||
return $this->signCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAttestedCredentialData(): ?AttestedCredentialData
|
||||
{
|
||||
return $this->attestedCredentialData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getExtensions(): ?AuthenticationExtensions
|
||||
{
|
||||
return $this->extensions !== null && $this->hasExtensions() ? $this->extensions : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use CBOR\ByteStringObject;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\ListObject;
|
||||
use CBOR\MapObject;
|
||||
use CBOR\NegativeIntegerObject;
|
||||
use CBOR\TextStringObject;
|
||||
use CBOR\UnsignedIntegerObject;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use function chr;
|
||||
use function ord;
|
||||
|
||||
final class AuthenticatorDataLoader
|
||||
{
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->decoder = Decoder::create();
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function load(string $authData): AuthenticatorData
|
||||
{
|
||||
$authData = $this->fixIncorrectEdDSAKey($authData);
|
||||
$authDataStream = new StringStream($authData);
|
||||
$rp_id_hash = $authDataStream->read(32);
|
||||
$flags = $authDataStream->read(1);
|
||||
$signCount = $authDataStream->read(4);
|
||||
$signCount = unpack('N', $signCount);
|
||||
|
||||
$attestedCredentialData = null;
|
||||
if (0 !== (ord($flags) & AuthenticatorData::FLAG_AT)) {
|
||||
$aaguid = Uuid::fromBinary($authDataStream->read(16));
|
||||
$credentialLength = $authDataStream->read(2);
|
||||
$credentialLength = unpack('n', $credentialLength);
|
||||
$credentialId = $authDataStream->read($credentialLength[1]);
|
||||
$credentialPublicKey = $this->decoder->decode($authDataStream);
|
||||
$credentialPublicKey instanceof MapObject || throw InvalidDataException::create(
|
||||
$authData,
|
||||
'The data does not contain a valid credential public key.'
|
||||
);
|
||||
$attestedCredentialData = AttestedCredentialData::create(
|
||||
$aaguid,
|
||||
$credentialId,
|
||||
(string) $credentialPublicKey
|
||||
);
|
||||
}
|
||||
|
||||
$extension = null;
|
||||
if (0 !== (ord($flags) & AuthenticatorData::FLAG_ED)) {
|
||||
$extension = $this->decoder->decode($authDataStream);
|
||||
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
|
||||
}
|
||||
$authDataStream->isEOF() || throw InvalidDataException::create(
|
||||
$authData,
|
||||
'Invalid authentication data. Presence of extra bytes.'
|
||||
);
|
||||
$authDataStream->close();
|
||||
|
||||
return AuthenticatorData::create(
|
||||
$authData,
|
||||
$rp_id_hash,
|
||||
$flags,
|
||||
$signCount[1],
|
||||
$attestedCredentialData,
|
||||
$extension
|
||||
);
|
||||
}
|
||||
|
||||
private function fixIncorrectEdDSAKey(string $data): string
|
||||
{
|
||||
$needle = hex2bin('a301634f4b500327206745643235353139');
|
||||
$correct = hex2bin('a401634f4b500327206745643235353139');
|
||||
$position = mb_strpos($data, $needle, 0, '8bit');
|
||||
if ($position === false) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$begin = mb_substr($data, 0, $position, '8bit');
|
||||
$end = mb_substr($data, $position, null, '8bit');
|
||||
$end = str_replace($needle, $correct, $end);
|
||||
$cbor = new StringStream($end);
|
||||
$badKey = $this->decoder->decode($cbor);
|
||||
|
||||
($badKey instanceof MapObject && $cbor->isEOF()) || throw InvalidDataException::create(
|
||||
$end,
|
||||
'Invalid authentication data. Presence of extra bytes.'
|
||||
);
|
||||
$badX = $badKey->get(-2);
|
||||
$badX instanceof ListObject || throw InvalidDataException::create($end, 'Invalid authentication data.');
|
||||
$keyBytes = array_reduce(
|
||||
$badX->normalize(),
|
||||
static fn (string $carry, string $item): string => $carry . chr((int) $item),
|
||||
''
|
||||
);
|
||||
$correctX = ByteStringObject::create($keyBytes);
|
||||
$correctKey = MapObject::create()
|
||||
->add(UnsignedIntegerObject::create(1), TextStringObject::create('OKP'))
|
||||
->add(UnsignedIntegerObject::create(3), NegativeIntegerObject::create(-8))
|
||||
->add(NegativeIntegerObject::create(-1), TextStringObject::create('Ed25519'))
|
||||
->add(NegativeIntegerObject::create(-2), $correctX);
|
||||
|
||||
return $begin . $correctKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#authenticatorresponse
|
||||
*/
|
||||
abstract class AuthenticatorResponse
|
||||
{
|
||||
public function __construct(
|
||||
public readonly CollectedClientData $clientDataJSON
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getClientDataJSON(): CollectedClientData
|
||||
{
|
||||
return $this->clientDataJSON;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use function in_array;
|
||||
use function is_bool;
|
||||
use function is_string;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
class AuthenticatorSelectionCriteria implements JsonSerializable
|
||||
{
|
||||
final public const AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE = null;
|
||||
|
||||
final public const AUTHENTICATOR_ATTACHMENT_PLATFORM = 'platform';
|
||||
|
||||
final public const AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 'cross-platform';
|
||||
|
||||
final public const AUTHENTICATOR_ATTACHMENTS = [
|
||||
self::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
|
||||
self::AUTHENTICATOR_ATTACHMENT_PLATFORM,
|
||||
self::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM,
|
||||
];
|
||||
|
||||
final public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
|
||||
|
||||
final public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
|
||||
|
||||
final public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
|
||||
|
||||
final public const USER_VERIFICATION_REQUIREMENTS = [
|
||||
self::USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
||||
self::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
|
||||
];
|
||||
|
||||
final public const RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE = null;
|
||||
|
||||
/**
|
||||
* @deprecated Please use AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE instead
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
final public const RESIDENT_KEY_REQUIREMENT_NONE = null;
|
||||
|
||||
final public const RESIDENT_KEY_REQUIREMENT_REQUIRED = 'required';
|
||||
|
||||
final public const RESIDENT_KEY_REQUIREMENT_PREFERRED = 'preferred';
|
||||
|
||||
final public const RESIDENT_KEY_REQUIREMENT_DISCOURAGED = 'discouraged';
|
||||
|
||||
final public const RESIDENT_KEY_REQUIREMENTS = [
|
||||
self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
|
||||
self::RESIDENT_KEY_REQUIREMENT_REQUIRED,
|
||||
self::RESIDENT_KEY_REQUIREMENT_PREFERRED,
|
||||
self::RESIDENT_KEY_REQUIREMENT_DISCOURAGED,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
public null|string $authenticatorAttachment = null,
|
||||
public string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
||||
public null|string $residentKey = self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
|
||||
/** @deprecated Will be removed in 5.0. Please use residentKey instead**/
|
||||
public null|bool $requireResidentKey = null,
|
||||
) {
|
||||
in_array($authenticatorAttachment, self::AUTHENTICATOR_ATTACHMENTS, true) || throw new InvalidArgumentException(
|
||||
'Invalid authenticator attachment'
|
||||
);
|
||||
in_array($userVerification, self::USER_VERIFICATION_REQUIREMENTS, true) || throw new InvalidArgumentException(
|
||||
'Invalid user verification'
|
||||
);
|
||||
in_array($residentKey, self::RESIDENT_KEY_REQUIREMENTS, true) || throw new InvalidArgumentException(
|
||||
'Invalid resident key'
|
||||
);
|
||||
if ($requireResidentKey === true && $residentKey !== null && $residentKey !== self::RESIDENT_KEY_REQUIREMENT_REQUIRED) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid resident key requirement. Resident key is required but requireResidentKey is false'
|
||||
);
|
||||
}
|
||||
if ($this->residentKey === null && $this->requireResidentKey === true) {
|
||||
$this->residentKey = self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
|
||||
}
|
||||
$this->requireResidentKey = $requireResidentKey ?? ($residentKey === null ? null : $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED);
|
||||
}
|
||||
|
||||
public static function create(
|
||||
?string $authenticatorAttachment = null,
|
||||
string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
||||
null|string $residentKey = self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
|
||||
null|bool $requireResidentKey = null
|
||||
): self {
|
||||
return new self($authenticatorAttachment, $userVerification, $residentKey, $requireResidentKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the {self::create} instead.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setAuthenticatorAttachment(?string $authenticatorAttachment): self
|
||||
{
|
||||
$this->authenticatorAttachment = $authenticatorAttachment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use the {self::create} instead.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setRequireResidentKey(bool $requireResidentKey): self
|
||||
{
|
||||
$this->requireResidentKey = $requireResidentKey;
|
||||
if ($requireResidentKey === true) {
|
||||
$this->residentKey = self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the {self::create} instead.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setUserVerification(string $userVerification): self
|
||||
{
|
||||
$this->userVerification = $userVerification;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the {self::create} instead.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function setResidentKey(null|string $residentKey): self
|
||||
{
|
||||
$this->residentKey = $residentKey;
|
||||
$this->requireResidentKey = $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getAuthenticatorAttachment(): ?string
|
||||
{
|
||||
return $this->authenticatorAttachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in 5.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function isRequireResidentKey(): bool
|
||||
{
|
||||
return $this->requireResidentKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getUserVerification(): string
|
||||
{
|
||||
return $this->userVerification;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getResidentKey(): null|string
|
||||
{
|
||||
return $this->residentKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
$authenticatorAttachment = $json['authenticatorAttachment'] ?? null;
|
||||
$requireResidentKey = $json['requireResidentKey'] ?? null;
|
||||
$userVerification = $json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED;
|
||||
$residentKey = $json['residentKey'] ?? null;
|
||||
|
||||
$authenticatorAttachment === null || is_string($authenticatorAttachment) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid "authenticatorAttachment" value'
|
||||
);
|
||||
($requireResidentKey === null || is_bool($requireResidentKey)) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid "requireResidentKey" value'
|
||||
);
|
||||
is_string($userVerification) || throw InvalidDataException::create($json, 'Invalid "userVerification" value');
|
||||
($residentKey === null || is_string($residentKey)) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid "residentKey" value'
|
||||
);
|
||||
|
||||
return self::create(
|
||||
$authenticatorAttachment ?? null,
|
||||
$userVerification,
|
||||
$residentKey,
|
||||
$requireResidentKey,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-bundle',
|
||||
'4.9.0',
|
||||
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
|
||||
__METHOD__
|
||||
);
|
||||
$json = [
|
||||
'requireResidentKey' => $this->requireResidentKey,
|
||||
'userVerification' => $this->userVerification,
|
||||
'residentKey' => $this->residentKey,
|
||||
'authenticatorAttachment' => $this->authenticatorAttachment,
|
||||
];
|
||||
foreach ($json as $key => $value) {
|
||||
if ($value === null) {
|
||||
unset($json[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
interface CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CeremonyStepManager
|
||||
{
|
||||
/**
|
||||
* @param CeremonyStep[] $steps
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $steps
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
foreach ($this->steps as $step) {
|
||||
$step->process(
|
||||
$publicKeyCredentialSource,
|
||||
$authenticatorResponse,
|
||||
$publicKeyCredentialOptions,
|
||||
$userHandle,
|
||||
$host
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Cose\Algorithm\Manager;
|
||||
use Cose\Algorithm\Signature\ECDSA\ES256;
|
||||
use Cose\Algorithm\Signature\RSA\RS256;
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\Counter\CounterChecker;
|
||||
use Webauthn\Counter\ThrowExceptionIfInvalid;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
|
||||
use Webauthn\MetadataService\MetadataStatementRepository;
|
||||
use Webauthn\MetadataService\StatusReportRepository;
|
||||
|
||||
final class CeremonyStepManagerFactory
|
||||
{
|
||||
private CounterChecker $counterChecker;
|
||||
|
||||
private Manager $algorithmManager;
|
||||
|
||||
private null|MetadataStatementRepository $metadataStatementRepository = null;
|
||||
|
||||
private null|StatusReportRepository $statusReportRepository = null;
|
||||
|
||||
private null|CertificateChainValidator $certificateChainValidator = null;
|
||||
|
||||
private null|TopOriginValidator $topOriginValidator = null;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private null|array $securedRelyingPartyId = null;
|
||||
|
||||
private AttestationStatementSupportManager $attestationStatementSupportManager;
|
||||
|
||||
private ExtensionOutputCheckerHandler $extensionOutputCheckerHandler;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->counterChecker = new ThrowExceptionIfInvalid();
|
||||
$this->algorithmManager = Manager::create()->add(ES256::create(), RS256::create());
|
||||
$this->attestationStatementSupportManager = new AttestationStatementSupportManager([
|
||||
new NoneAttestationStatementSupport(),
|
||||
]);
|
||||
$this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
|
||||
}
|
||||
|
||||
public function setCounterChecker(CounterChecker $counterChecker): void
|
||||
{
|
||||
$this->counterChecker = $counterChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $securedRelyingPartyId
|
||||
*/
|
||||
public function setSecuredRelyingPartyId(array $securedRelyingPartyId): void
|
||||
{
|
||||
$this->securedRelyingPartyId = $securedRelyingPartyId;
|
||||
}
|
||||
|
||||
public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void
|
||||
{
|
||||
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
|
||||
}
|
||||
|
||||
public function setAttestationStatementSupportManager(
|
||||
AttestationStatementSupportManager $attestationStatementSupportManager
|
||||
): void {
|
||||
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
|
||||
}
|
||||
|
||||
public function setAlgorithmManager(Manager $algorithmManager): void
|
||||
{
|
||||
$this->algorithmManager = $algorithmManager;
|
||||
}
|
||||
|
||||
public function enableMetadataStatementSupport(
|
||||
MetadataStatementRepository $metadataStatementRepository,
|
||||
StatusReportRepository $statusReportRepository,
|
||||
CertificateChainValidator $certificateChainValidator
|
||||
): void {
|
||||
$this->metadataStatementRepository = $metadataStatementRepository;
|
||||
$this->statusReportRepository = $statusReportRepository;
|
||||
$this->certificateChainValidator = $certificateChainValidator;
|
||||
}
|
||||
|
||||
public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void
|
||||
{
|
||||
$this->certificateChainValidator = $certificateChainValidator;
|
||||
}
|
||||
|
||||
public function enableTopOriginValidator(TopOriginValidator $topOriginValidator): void
|
||||
{
|
||||
$this->topOriginValidator = $topOriginValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string[] $securedRelyingPartyId
|
||||
*/
|
||||
public function creationCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager
|
||||
{
|
||||
$metadataStatementChecker = new CheckMetadataStatement();
|
||||
if ($this->certificateChainValidator !== null) {
|
||||
$metadataStatementChecker->enableCertificateChainValidator($this->certificateChainValidator);
|
||||
}
|
||||
if ($this->metadataStatementRepository !== null && $this->statusReportRepository !== null && $this->certificateChainValidator !== null) {
|
||||
$metadataStatementChecker->enableMetadataStatementSupport(
|
||||
$this->metadataStatementRepository,
|
||||
$this->statusReportRepository,
|
||||
$this->certificateChainValidator,
|
||||
);
|
||||
}
|
||||
|
||||
/* @see https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential */
|
||||
return new CeremonyStepManager([
|
||||
new CheckClientDataCollectorType(),
|
||||
new CheckChallenge(),
|
||||
new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []),
|
||||
new CheckTopOrigin($this->topOriginValidator),
|
||||
new CheckRelyingPartyIdIdHash(),
|
||||
new CheckUserWasPresent(),
|
||||
new CheckUserVerification(),
|
||||
new CheckBackupBitsAreConsistent(),
|
||||
new CheckAlgorithm(),
|
||||
new CheckExtensions($this->extensionOutputCheckerHandler),
|
||||
new CheckAttestationFormatIsKnownAndValid($this->attestationStatementSupportManager),
|
||||
new CheckHasAttestedCredentialData(),
|
||||
$metadataStatementChecker,
|
||||
new CheckCredentialId(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|string[] $securedRelyingPartyId
|
||||
*/
|
||||
public function requestCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager
|
||||
{
|
||||
/* @see https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion */
|
||||
return new CeremonyStepManager([
|
||||
new CheckAllowedCredentialList(),
|
||||
new CheckUserHandle(),
|
||||
new CheckClientDataCollectorType(),
|
||||
new CheckChallenge(),
|
||||
new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []),
|
||||
new CheckTopOrigin(null),
|
||||
new CheckRelyingPartyIdIdHash(),
|
||||
new CheckUserWasPresent(),
|
||||
new CheckUserVerification(),
|
||||
new CheckBackupBitsAreConsistent(),
|
||||
new CheckExtensions($this->extensionOutputCheckerHandler),
|
||||
new CheckSignature($this->algorithmManager),
|
||||
new CheckCounter($this->counterChecker),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Key;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\U2FPublicKey;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
class CheckAlgorithm implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
if (! $publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions) {
|
||||
return;
|
||||
}
|
||||
$credentialPublicKey = $publicKeyCredentialSource->getAttestedCredentialData()
|
||||
->credentialPublicKey;
|
||||
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'No public key available.'
|
||||
);
|
||||
$algorithms = array_map(
|
||||
fn ($pubKeyCredParam) => $pubKeyCredParam->alg,
|
||||
$publicKeyCredentialOptions->pubKeyCredParams
|
||||
);
|
||||
if (count($algorithms) === 0) {
|
||||
$algorithms = [Algorithms::COSE_ALGORITHM_ES256, Algorithms::COSE_ALGORITHM_RS256];
|
||||
}
|
||||
$coseKey = $this->getCoseKey($credentialPublicKey);
|
||||
in_array($coseKey->alg(), $algorithms, true) || throw AuthenticatorResponseVerificationException::create(
|
||||
sprintf('Invalid algorithm. Expected one of %s but got %d', implode(', ', $algorithms), $coseKey->alg())
|
||||
);
|
||||
}
|
||||
|
||||
private function getCoseKey(string $credentialPublicKey): Key
|
||||
{
|
||||
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
|
||||
if ($isU2F === true) {
|
||||
$credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey);
|
||||
}
|
||||
$stream = new StringStream($credentialPublicKey);
|
||||
$credentialPublicKeyStream = Decoder::create()->decode($stream);
|
||||
$stream->isEOF() || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid key. Presence of extra bytes.'
|
||||
);
|
||||
$stream->close();
|
||||
$credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid attestation object. Unexpected object.'
|
||||
);
|
||||
$normalizedData = $credentialPublicKeyStream->normalize();
|
||||
is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid attestation object. Unexpected object.'
|
||||
);
|
||||
/** @var array<int|string, mixed> $normalizedData */
|
||||
|
||||
return Key::create($normalizedData);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use function count;
|
||||
|
||||
final class CheckAllowedCredentialList implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
if (! $publicKeyCredentialOptions instanceof PublicKeyCredentialRequestOptions) {
|
||||
return;
|
||||
}
|
||||
if (count($publicKeyCredentialOptions->allowCredentials) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($publicKeyCredentialOptions->allowCredentials as $allowedCredential) {
|
||||
if (hash_equals($allowedCredential->id, $publicKeyCredentialSource->publicKeyCredentialId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw AuthenticatorResponseVerificationException::create('The credential ID is not allowed.');
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckAttestationFormatIsKnownAndValid implements CeremonyStep
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AttestationStatementSupportManager $attestationStatementSupportManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$attestationObject = $authenticatorResponse->attestationObject;
|
||||
if ($attestationObject === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fmt = $attestationObject->attStmt
|
||||
->fmt;
|
||||
$this->attestationStatementSupportManager->has(
|
||||
$fmt
|
||||
) || throw AuthenticatorResponseVerificationException::create('Unsupported attestation statement format.');
|
||||
|
||||
$attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
|
||||
$clientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON ->rawData, true);
|
||||
$attestationStatementSupport->isValid(
|
||||
$clientDataJSONHash,
|
||||
$attestationObject->attStmt,
|
||||
$attestationObject->authData
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid attestation statement.');
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckBackupBitsAreConsistent implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
if ($authData->isBackupEligible()) {
|
||||
return;
|
||||
}
|
||||
$authData->isBackedUp() !== true || throw AuthenticatorResponseVerificationException::create(
|
||||
'Backup up bit is set but the backup is not eligible.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckChallenge implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$publicKeyCredentialOptions->challenge !== '' || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid challenge.'
|
||||
);
|
||||
hash_equals(
|
||||
$publicKeyCredentialOptions->challenge,
|
||||
$authenticatorResponse->clientDataJSON->challenge
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.');
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\ClientDataCollector\ClientDataCollectorManager;
|
||||
use Webauthn\ClientDataCollector\WebauthnAuthenticationCollector;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckClientDataCollectorType implements CeremonyStep
|
||||
{
|
||||
private readonly ClientDataCollectorManager $clientDataCollectorManager;
|
||||
|
||||
public function __construct(
|
||||
null|ClientDataCollectorManager $clientDataCollectorManager = null,
|
||||
) {
|
||||
$this->clientDataCollectorManager = $clientDataCollectorManager ?? new ClientDataCollectorManager([
|
||||
new WebauthnAuthenticationCollector(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$this->clientDataCollectorManager->collect(
|
||||
$authenticatorResponse->clientDataJSON,
|
||||
$publicKeyCredentialOptions,
|
||||
$authenticatorResponse,
|
||||
$host
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Counter\CounterChecker;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckCounter implements CeremonyStep
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CounterChecker $counterChecker
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$storedCounter = $publicKeyCredentialSource->counter;
|
||||
$responseCounter = $authData->signCount;
|
||||
if ($responseCounter !== 0 || $storedCounter !== 0) {
|
||||
$this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
|
||||
}
|
||||
$publicKeyCredentialSource->counter = $responseCounter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
class CheckCredentialId implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$credentialId = $publicKeyCredentialSource->publicKeyCredentialId;
|
||||
mb_strlen($credentialId) <= 1023 || throw new AuthenticatorResponseVerificationException(
|
||||
'Credential ID too long.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckExtensions implements CeremonyStep
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExtensionOutputCheckerHandler $extensionOutputCheckerHandler,
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$extensionsClientOutputs = $authData->extensions;
|
||||
if ($extensionsClientOutputs !== null) {
|
||||
$this->extensionOutputCheckerHandler->check(
|
||||
$publicKeyCredentialOptions->extensions,
|
||||
$extensionsClientOutputs
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckHasAttestedCredentialData implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$authData
|
||||
->hasAttestedCredentialData() || throw AuthenticatorResponseVerificationException::create(
|
||||
'There is no attested credential data.'
|
||||
);
|
||||
$authData->attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'There is no attested credential data.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Webauthn\AttestationStatement\AttestationStatement;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\MetadataStatementRepository;
|
||||
use Webauthn\MetadataService\Statement\MetadataStatement;
|
||||
use Webauthn\MetadataService\StatusReportRepository;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use function count;
|
||||
use function in_array;
|
||||
|
||||
final class CheckMetadataStatement implements CeremonyStep, CanLogData
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private null|MetadataStatementRepository $metadataStatementRepository = null;
|
||||
|
||||
private null|StatusReportRepository $statusReportRepository = null;
|
||||
|
||||
private null|CertificateChainValidator $certificateChainValidator = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public function enableMetadataStatementSupport(
|
||||
MetadataStatementRepository $metadataStatementRepository,
|
||||
StatusReportRepository $statusReportRepository,
|
||||
CertificateChainValidator $certificateChainValidator
|
||||
): void {
|
||||
$this->metadataStatementRepository = $metadataStatementRepository;
|
||||
$this->statusReportRepository = $statusReportRepository;
|
||||
$this->certificateChainValidator = $certificateChainValidator;
|
||||
}
|
||||
|
||||
public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void
|
||||
{
|
||||
$this->certificateChainValidator = $certificateChainValidator;
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
if (
|
||||
! $publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions
|
||||
|| ! $authenticatorResponse instanceof AuthenticatorAttestationResponse
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$attestationStatement = $authenticatorResponse->attestationObject->attStmt;
|
||||
$attestedCredentialData = $authenticatorResponse->attestationObject->authData
|
||||
->attestedCredentialData;
|
||||
$attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'No attested credential data found'
|
||||
);
|
||||
$aaguid = $attestedCredentialData->aaguid
|
||||
->__toString();
|
||||
if ($publicKeyCredentialOptions->attestation === null || $publicKeyCredentialOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) {
|
||||
$this->logger->debug('No attestation is asked.');
|
||||
if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array(
|
||||
$attestationStatement->type,
|
||||
[AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF],
|
||||
true
|
||||
)) {
|
||||
$this->logger->debug('The Attestation Statement is anonymous.');
|
||||
$this->checkCertificateChain($attestationStatement, null);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
|
||||
// => nothing to check
|
||||
if ($attestationStatement->type === AttestationStatement::TYPE_NONE) {
|
||||
$this->logger->debug('No attestation returned.');
|
||||
//No attestation is returned. We shall ensure that the AAGUID is a null one.
|
||||
//if ($aaguid !== '00000000-0000-0000-0000-000000000000') {
|
||||
//$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
|
||||
// 'aaguid' => $aaguid,
|
||||
// 'AttestationStatement' => $attestationStatement,
|
||||
//]);
|
||||
//$attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000');
|
||||
// return;
|
||||
//}
|
||||
return;
|
||||
}
|
||||
if ($aaguid === '00000000-0000-0000-0000-000000000000') {
|
||||
//No need to continue if the AAGUID is null.
|
||||
// This could be the case e.g. with AnonCA type
|
||||
return;
|
||||
}
|
||||
//The MDS Repository is mandatory here
|
||||
$this->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'The Metadata Statement Repository is mandatory when requesting attestation objects.'
|
||||
);
|
||||
$metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
|
||||
// At this point, the Metadata Statement is mandatory
|
||||
$metadataStatement !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid)
|
||||
);
|
||||
// We check the last status report
|
||||
$this->checkStatusReport($aaguid);
|
||||
// We check the certificate chain (if any)
|
||||
$this->checkCertificateChain($attestationStatement, $metadataStatement);
|
||||
// Check Attestation Type is allowed
|
||||
if (count($metadataStatement->attestationTypes) !== 0) {
|
||||
$type = $this->getAttestationType($attestationStatement);
|
||||
in_array(
|
||||
$type,
|
||||
$metadataStatement->attestationTypes,
|
||||
true
|
||||
) || throw AuthenticatorResponseVerificationException::create(
|
||||
sprintf(
|
||||
'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator.',
|
||||
$type
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getAttestationType(AttestationStatement $attestationStatement): string
|
||||
{
|
||||
return match ($attestationStatement->type) {
|
||||
AttestationStatement::TYPE_BASIC => MetadataStatement::ATTESTATION_BASIC_FULL,
|
||||
AttestationStatement::TYPE_SELF => MetadataStatement::ATTESTATION_BASIC_SURROGATE,
|
||||
AttestationStatement::TYPE_ATTCA => MetadataStatement::ATTESTATION_ATTCA,
|
||||
AttestationStatement::TYPE_ECDAA => MetadataStatement::ATTESTATION_ECDAA,
|
||||
AttestationStatement::TYPE_ANONCA => MetadataStatement::ATTESTATION_ANONCA,
|
||||
default => throw AuthenticatorResponseVerificationException::create('Invalid attestation type'),
|
||||
};
|
||||
}
|
||||
|
||||
private function checkStatusReport(string $aaguid): void
|
||||
{
|
||||
$statusReports = $this->statusReportRepository === null ? [] : $this->statusReportRepository->findStatusReportsByAAGUID(
|
||||
$aaguid
|
||||
);
|
||||
if (count($statusReports) !== 0) {
|
||||
$lastStatusReport = end($statusReports);
|
||||
if ($lastStatusReport->isCompromised()) {
|
||||
throw AuthenticatorResponseVerificationException::create(
|
||||
'The authenticator is compromised and cannot be used'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkCertificateChain(
|
||||
AttestationStatement $attestationStatement,
|
||||
?MetadataStatement $metadataStatement
|
||||
): void {
|
||||
$trustPath = $attestationStatement->trustPath;
|
||||
if (! $trustPath instanceof CertificateTrustPath) {
|
||||
return;
|
||||
}
|
||||
$authenticatorCertificates = $trustPath->certificates;
|
||||
if ($metadataStatement === null) {
|
||||
$this->certificateChainValidator?->check($authenticatorCertificates, []);
|
||||
return;
|
||||
}
|
||||
$trustedCertificates = CertificateToolbox::fixPEMStructures(
|
||||
$metadataStatement->attestationRootCertificates
|
||||
);
|
||||
$this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
|
||||
final class CheckOrigin implements CeremonyStep
|
||||
{
|
||||
/**
|
||||
* @param string[] $securedRelyingPartyId
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $securedRelyingPartyId
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$C = $authenticatorResponse->clientDataJSON;
|
||||
$rpId = $publicKeyCredentialOptions->rpId ?? $publicKeyCredentialOptions->rp->id ?? $host;
|
||||
$facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData->extensions);
|
||||
$parsedRelyingPartyId = parse_url($C->origin);
|
||||
is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid origin'
|
||||
);
|
||||
if (! in_array($facetId, $this->securedRelyingPartyId, true)) {
|
||||
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
|
||||
$scheme === 'https' || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid scheme. HTTPS required.'
|
||||
);
|
||||
}
|
||||
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
|
||||
$clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.');
|
||||
$rpIdLength = mb_strlen($facetId);
|
||||
|
||||
mb_substr(
|
||||
'.' . $clientDataRpId,
|
||||
-($rpIdLength + 1)
|
||||
) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.');
|
||||
}
|
||||
|
||||
private function getFacetId(
|
||||
string $rpId,
|
||||
AuthenticationExtensions $authenticationExtensionsClientInputs,
|
||||
null|AuthenticationExtensions $authenticationExtensionsClientOutputs
|
||||
): string {
|
||||
if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has(
|
||||
'appid'
|
||||
) || ! $authenticationExtensionsClientOutputs->has('appid')) {
|
||||
return $rpId;
|
||||
}
|
||||
$appId = $authenticationExtensionsClientInputs->get('appid')
|
||||
->value;
|
||||
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')
|
||||
->value;
|
||||
if (! is_string($appId) || $wasUsed !== true) {
|
||||
return $rpId;
|
||||
}
|
||||
return $appId;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\U2FPublicKey;
|
||||
use function is_string;
|
||||
|
||||
final class CheckRelyingPartyIdIdHash implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$C = $authenticatorResponse->clientDataJSON;
|
||||
$attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
|
||||
$credentialPublicKey = $attestedCredentialData->credentialPublicKey;
|
||||
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'No public key available.'
|
||||
);
|
||||
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
|
||||
$rpId = $publicKeyCredentialOptions->rpId ?? $publicKeyCredentialOptions->rp->id ?? $host;
|
||||
$facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData ->extensions);
|
||||
$rpIdHash = hash('sha256', $isU2F ? $C->origin : $facetId, true);
|
||||
hash_equals(
|
||||
$rpIdHash,
|
||||
$authData
|
||||
->rpIdHash
|
||||
) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.');
|
||||
}
|
||||
|
||||
private function getFacetId(
|
||||
string $rpId,
|
||||
AuthenticationExtensions $authenticationExtensionsClientInputs,
|
||||
null|AuthenticationExtensions $authenticationExtensionsClientOutputs
|
||||
): string {
|
||||
if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has(
|
||||
'appid'
|
||||
) || ! $authenticationExtensionsClientOutputs->has('appid')) {
|
||||
return $rpId;
|
||||
}
|
||||
$appId = $authenticationExtensionsClientInputs->get('appid')
|
||||
->value;
|
||||
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')
|
||||
->value;
|
||||
if (! is_string($appId) || $wasUsed !== true) {
|
||||
return $rpId;
|
||||
}
|
||||
return $appId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Algorithm\Manager;
|
||||
use Cose\Algorithm\Signature\ECDSA\ES256;
|
||||
use Cose\Algorithm\Signature\RSA\RS256;
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Key\Key;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\U2FPublicKey;
|
||||
use Webauthn\Util\CoseSignatureFixer;
|
||||
use function is_array;
|
||||
|
||||
final class CheckSignature implements CeremonyStep
|
||||
{
|
||||
private readonly Manager $algorithmManager;
|
||||
|
||||
public function __construct(
|
||||
null|Manager $algorithmManager = null,
|
||||
) {
|
||||
$this->algorithmManager = $algorithmManager ?? Manager::create()->add(ES256::create(), RS256::create());
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
if (! $authenticatorResponse instanceof AuthenticatorAssertionResponse) {
|
||||
return;
|
||||
}
|
||||
$credentialPublicKey = $publicKeyCredentialSource->getAttestedCredentialData()
|
||||
->credentialPublicKey;
|
||||
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'No public key available.'
|
||||
);
|
||||
$coseKey = $this->getCoseKey($credentialPublicKey);
|
||||
|
||||
$getClientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON->rawData, true);
|
||||
$dataToVerify = $authenticatorResponse->authenticatorData->authData . $getClientDataJSONHash;
|
||||
$signature = $authenticatorResponse->signature;
|
||||
$algorithm = $this->algorithmManager->get($coseKey->alg());
|
||||
$algorithm instanceof Signature || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid algorithm identifier. Should refer to a signature algorithm'
|
||||
);
|
||||
$signature = CoseSignatureFixer::fix($signature, $algorithm);
|
||||
$algorithm->verify(
|
||||
$dataToVerify,
|
||||
$coseKey,
|
||||
$signature
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid signature.');
|
||||
}
|
||||
|
||||
private function getCoseKey(string $credentialPublicKey): Key
|
||||
{
|
||||
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
|
||||
if ($isU2F === true) {
|
||||
$credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey);
|
||||
}
|
||||
$stream = new StringStream($credentialPublicKey);
|
||||
$credentialPublicKeyStream = Decoder::create()->decode($stream);
|
||||
$stream->isEOF() || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid key. Presence of extra bytes.'
|
||||
);
|
||||
$stream->close();
|
||||
$credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid attestation object. Unexpected object.'
|
||||
);
|
||||
$normalizedData = $credentialPublicKeyStream->normalize();
|
||||
is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid attestation object. Unexpected object.'
|
||||
);
|
||||
/** @var array<int|string, mixed> $normalizedData */
|
||||
|
||||
return Key::create($normalizedData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
class CheckTopOrigin implements CeremonyStep
|
||||
{
|
||||
public function __construct(
|
||||
private readonly null|TopOriginValidator $topOriginValidator = null
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$topOrigin = $authenticatorResponse->clientDataJSON->topOrigin;
|
||||
if ($topOrigin === null) {
|
||||
return;
|
||||
}
|
||||
if ($authenticatorResponse->clientDataJSON->crossOrigin !== true) {
|
||||
throw AuthenticatorResponseVerificationException::create('The response is not cross-origin.');
|
||||
}
|
||||
if ($this->topOriginValidator === null) {
|
||||
(new HostTopOriginValidator($host))->validate($topOrigin);
|
||||
} else {
|
||||
$this->topOriginValidator->validate($topOrigin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\InvalidUserHandleException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckUserHandle implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
if (! $authenticatorResponse instanceof AuthenticatorAssertionResponse) {
|
||||
return;
|
||||
}
|
||||
$credentialUserHandle = $publicKeyCredentialSource->userHandle;
|
||||
$responseUserHandle = $authenticatorResponse->userHandle;
|
||||
if ($userHandle !== null) { //If the user was identified before the authentication ceremony was initiated,
|
||||
$credentialUserHandle === $userHandle || throw InvalidUserHandleException::create();
|
||||
if ($responseUserHandle !== null && $responseUserHandle !== '') {
|
||||
$credentialUserHandle === $responseUserHandle || throw InvalidUserHandleException::create();
|
||||
}
|
||||
} else {
|
||||
($responseUserHandle !== '' && $credentialUserHandle === $responseUserHandle) || throw InvalidUserHandleException::create();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\AuthenticatorSelectionCriteria;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckUserVerification implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$userVerification = $publicKeyCredentialOptions instanceof PublicKeyCredentialRequestOptions ? $publicKeyCredentialOptions->userVerification : $publicKeyCredentialOptions->authenticatorSelection?->userVerification;
|
||||
if ($userVerification !== AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) {
|
||||
return;
|
||||
}
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$authData->isUserVerified() || throw AuthenticatorResponseVerificationException::create(
|
||||
'User authentication required.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class CheckUserWasPresent implements CeremonyStep
|
||||
{
|
||||
public function process(
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource,
|
||||
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
|
||||
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
|
||||
?string $userHandle,
|
||||
string $host
|
||||
): void {
|
||||
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
|
||||
$authData->isUserPresent() || throw AuthenticatorResponseVerificationException::create('User was not present');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
|
||||
final class HostTopOriginValidator implements TopOriginValidator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $host
|
||||
) {
|
||||
}
|
||||
|
||||
public function validate(string $topOrigin): void
|
||||
{
|
||||
$topOrigin === $this->host || throw AuthenticatorResponseVerificationException::create(
|
||||
'The top origin does not correspond to the host.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CeremonyStep;
|
||||
|
||||
interface TopOriginValidator
|
||||
{
|
||||
public function validate(string $topOrigin): void;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CertificateChainChecker;
|
||||
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\CertificateChainValidator instead
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
interface CertificateChainChecker extends CertificateChainValidator
|
||||
{
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CertificateChainChecker;
|
||||
|
||||
use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator;
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\PhpCertificateChainValidator instead
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
final class PhpCertificateChainChecker extends PhpCertificateChainValidator
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox as BaseCertificateToolbox;
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\PhpCertificateChainValidator instead
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
class CertificateToolbox extends BaseCertificateToolbox
|
||||
{
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\ClientDataCollector;
|
||||
|
||||
use Webauthn\AuthenticatorResponse;
|
||||
use Webauthn\CollectedClientData;
|
||||
use Webauthn\PublicKeyCredentialOptions;
|
||||
|
||||
interface ClientDataCollector
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function supportedTypes(): array;
|
||||
|
||||
public function verifyCollectedClientData(
|
||||
CollectedClientData $collectedClientData,
|
||||
PublicKeyCredentialOptions $publicKeyCredentialOptions,
|
||||
AuthenticatorResponse $authenticatorResponse,
|
||||
string $host
|
||||
): void;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\ClientDataCollector;
|
||||
|
||||
use Webauthn\AuthenticatorResponse;
|
||||
use Webauthn\CollectedClientData;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialOptions;
|
||||
use function in_array;
|
||||
|
||||
final class ClientDataCollectorManager
|
||||
{
|
||||
/**
|
||||
* @param ClientDataCollector[] $clientDataCollectors
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly iterable $clientDataCollectors,
|
||||
) {
|
||||
}
|
||||
|
||||
public function collect(
|
||||
CollectedClientData $collectedClientData,
|
||||
PublicKeyCredentialOptions $publicKeyCredentialOptions,
|
||||
AuthenticatorResponse $authenticatorResponse,
|
||||
string $host
|
||||
): void {
|
||||
foreach ($this->clientDataCollectors as $clientDataCollector) {
|
||||
if (in_array($collectedClientData->type, $clientDataCollector->supportedTypes(), true)) {
|
||||
$clientDataCollector->verifyCollectedClientData(
|
||||
$collectedClientData,
|
||||
$publicKeyCredentialOptions,
|
||||
$authenticatorResponse,
|
||||
$host
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw AuthenticatorResponseVerificationException::create('No client data collector found.');
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\ClientDataCollector;
|
||||
|
||||
use Webauthn\AuthenticatorResponse;
|
||||
use Webauthn\CollectedClientData;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\PublicKeyCredentialOptions;
|
||||
use function in_array;
|
||||
|
||||
final class WebauthnAuthenticationCollector implements ClientDataCollector
|
||||
{
|
||||
public function supportedTypes(): array
|
||||
{
|
||||
return ['webauthn.get', 'webauthn.create'];
|
||||
}
|
||||
|
||||
public function verifyCollectedClientData(
|
||||
CollectedClientData $collectedClientData,
|
||||
PublicKeyCredentialOptions $publicKeyCredentialOptions,
|
||||
AuthenticatorResponse $authenticatorResponse,
|
||||
string $host
|
||||
): void {
|
||||
in_array(
|
||||
$collectedClientData->type,
|
||||
$this->supportedTypes(),
|
||||
true
|
||||
) || throw AuthenticatorResponseVerificationException::create(
|
||||
sprintf('The client data type is not "%s" supported.', implode('", "', $this->supportedTypes()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\TokenBinding\TokenBinding;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
class CollectedClientData
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
public readonly array $data;
|
||||
|
||||
public readonly string $type;
|
||||
|
||||
public readonly string $challenge;
|
||||
|
||||
public readonly string $origin;
|
||||
|
||||
public readonly null|string $topOrigin;
|
||||
|
||||
public readonly bool $crossOrigin;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public readonly ?array $tokenBinding;
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $rawData,
|
||||
array $data
|
||||
) {
|
||||
$type = $data['type'] ?? '';
|
||||
(is_string($type) && $type !== '') || throw InvalidDataException::create(
|
||||
$data,
|
||||
'Invalid parameter "type". Shall be a non-empty string.'
|
||||
);
|
||||
$this->type = $type;
|
||||
|
||||
$challenge = $data['challenge'] ?? '';
|
||||
is_string($challenge) || throw InvalidDataException::create(
|
||||
$data,
|
||||
'Invalid parameter "challenge". Shall be a string.'
|
||||
);
|
||||
$challenge = Base64UrlSafe::decodeNoPadding($challenge);
|
||||
$challenge !== '' || throw InvalidDataException::create(
|
||||
$data,
|
||||
'Invalid parameter "challenge". Shall not be empty.'
|
||||
);
|
||||
$this->challenge = $challenge;
|
||||
|
||||
$origin = $data['origin'] ?? '';
|
||||
(is_string($origin) && $origin !== '') || throw InvalidDataException::create(
|
||||
$data,
|
||||
'Invalid parameter "origin". Shall be a non-empty string.'
|
||||
);
|
||||
$this->origin = $origin;
|
||||
|
||||
$this->topOrigin = $data['topOrigin'] ?? null;
|
||||
$this->crossOrigin = $data['crossOrigin'] ?? false;
|
||||
|
||||
$tokenBinding = $data['tokenBinding'] ?? null;
|
||||
$tokenBinding === null || is_array($tokenBinding) || throw InvalidDataException::create(
|
||||
$data,
|
||||
'Invalid parameter "tokenBinding". Shall be an object or .'
|
||||
);
|
||||
$this->tokenBinding = $tokenBinding;
|
||||
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public static function create(string $rawData, array $data): self
|
||||
{
|
||||
return new self($rawData, $data);
|
||||
}
|
||||
|
||||
public static function createFormJson(string $data): self
|
||||
{
|
||||
$rawData = Base64UrlSafe::decodeNoPadding($data);
|
||||
$json = json_decode($rawData, true, flags: JSON_THROW_ON_ERROR);
|
||||
is_array($json) || throw InvalidDataException::create($data, 'Invalid JSON data.');
|
||||
|
||||
return self::create($rawData, $json);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getChallenge(): string
|
||||
{
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getOrigin(): string
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getCrossOrigin(): bool
|
||||
{
|
||||
return $this->crossOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
public function getTokenBinding(): ?TokenBinding
|
||||
{
|
||||
return $this->tokenBinding === null ? null : TokenBinding::createFormArray($this->tokenBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 4.7.0. Please use the property directly.
|
||||
* @infection-ignore-all
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
public function get(string $key): mixed
|
||||
{
|
||||
if (! $this->has($key)) {
|
||||
throw InvalidDataException::create($this->data, sprintf('The key "%s" is missing', $key));
|
||||
}
|
||||
|
||||
return $this->data[$key];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Counter;
|
||||
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
interface CounterChecker
|
||||
{
|
||||
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Counter;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Webauthn\Exception\CounterException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
|
||||
final class ThrowExceptionIfInvalid implements CounterChecker, CanLogData
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger = new NullLogger()
|
||||
) {
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void
|
||||
{
|
||||
try {
|
||||
$currentCounter > $publicKeyCredentialSource->counter || throw CounterException::create(
|
||||
$currentCounter,
|
||||
$publicKeyCredentialSource->counter,
|
||||
'Invalid counter.'
|
||||
);
|
||||
} catch (CounterException $throwable) {
|
||||
$this->logger->error('The counter is invalid', [
|
||||
'current' => $currentCounter,
|
||||
'new' => $publicKeyCredentialSource->counter,
|
||||
]);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user