Обновление клиента (apps, 3rdparty, install)

This commit is contained in:
root
2026-03-16 08:42:57 +00:00
parent b8905de237
commit f390426546
3354 changed files with 505213 additions and 3 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+10
View File
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Cose\Algorithm;
interface Algorithm
{
public static function identifier(): int;
}
+30
View File
@@ -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;
}
}
+30
View File
@@ -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;
}
}
+30
View File
@@ -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;
}
}
+43
View File
@@ -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');
}
}
}
+15
View File
@@ -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;
}
+61
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+44
View File
@@ -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
View File
@@ -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.
@@ -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');
}
}
}
@@ -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);
}
}
@@ -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');
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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,
];
}
}
@@ -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;
}
@@ -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];
}
}
@@ -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'
);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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]);
}
}
@@ -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
{
}
@@ -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
{
}
@@ -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)
)
);
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
interface ExtensionOutputChecker
{
public function check(AuthenticationExtensions $inputs, AuthenticationExtensions $outputs): void;
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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,
);
}
}
+140
View File
@@ -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
);
}
}
}
@@ -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);
}
}
@@ -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.');
}
}
@@ -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.');
}
}
@@ -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.');
}
}
@@ -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
);
}
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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
{
}
@@ -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
{
}
@@ -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;
}
@@ -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.');
}
}
@@ -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