Обновление клиента (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
@@ -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;
}
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use InvalidArgumentException;
use ParagonIE\ConstantTime\Base64UrlSafe;
/**
* @see https://w3c.github.io/webappsec-credential-management/#credential
*/
abstract class Credential
{
/**
* @deprecated since 4.9.0. Please use the property rawId instead.
*/
public readonly string $id;
public readonly string $rawId;
public function __construct(
null|string $id,
public readonly string $type,
null|string $rawId = null,
) {
if ($id === null && $rawId === null) {
throw new InvalidArgumentException('You must provide a valid raw ID');
}
if ($id !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.9.0',
'The property "$id" is deprecated and will be removed in 5.0.0. Please set null use "rawId" instead.'
);
} else {
$id = Base64UrlSafe::encodeUnpadded($rawId);
}
$this->id = $id;
$this->rawId = $rawId ?? Base64UrlSafe::decodeNoPadding($id);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getId(): string
{
return $this->id;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getType(): string
{
return $this->type;
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use CBOR\Decoder;
use CBOR\Normalizable;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\AttestationStatement\AttestationObject;
use Webauthn\AttestationStatement\AttestationStatement;
use Webauthn\AuthenticatorData;
use Webauthn\Exception\InvalidDataException;
use Webauthn\StringStream;
final class AttestationObjectDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$stream = new StringStream($data);
$parsed = Decoder::create()->decode($stream);
$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();
$authData = $attestationObject['authData'] ?? throw InvalidDataException::create(
$attestationObject,
'Invalid attestation object. Missing "authData" field.'
);
return AttestationObject::create(
$data,
$this->denormalizer->denormalize($attestationObject, AttestationStatement::class, $format, $context),
$this->denormalizer->denormalize($authData, AuthenticatorData::class, $format, $context),
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === AttestationObject::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AttestationObject::class => true,
];
}
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\AttestationStatement\AttestationStatement;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
final class AttestationStatementDenormalizer implements DenormalizerInterface
{
public function __construct(
private readonly AttestationStatementSupportManager $attestationStatementSupportManager
) {
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$attestationStatementSupport = $this->attestationStatementSupportManager->get($data['fmt']);
return $attestationStatementSupport->load($data);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === AttestationStatement::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AttestationStatement::class => true,
];
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\AttestedCredentialData;
use function assert;
final class AttestedCredentialDataNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof AttestedCredentialData);
$result = [
'aaguid' => $this->normalizer->normalize($data->aaguid, $format, $context),
'credentialId' => base64_encode($data->credentialId),
];
if ($data->credentialPublicKey !== null) {
$result['credentialPublicKey'] = base64_encode($data->credentialPublicKey);
}
return $result;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof AttestedCredentialData;
}
public function getSupportedTypes(?string $format): array
{
return [
AttestedCredentialData::class => true,
];
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use function assert;
final class AuthenticationExtensionNormalizer implements NormalizerInterface
{
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AuthenticationExtension::class => true,
];
}
/**
* @return array<mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof AuthenticationExtension);
return $data->value;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof AuthenticationExtension;
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\AuthenticationExtensions\AuthenticationExtension;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
use function assert;
use function in_array;
use function is_array;
use function is_string;
final class AuthenticationExtensionsDenormalizer implements DenormalizerInterface, NormalizerInterface
{
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if ($data instanceof AuthenticationExtensions) {
return AuthenticationExtensions::create($data->extensions);
}
assert(is_array($data), 'The data should be an array.');
foreach ($data as $key => $value) {
if (! is_string($key)) {
continue;
}
$data[$key] = AuthenticationExtension::create($key, $value);
}
return AuthenticationExtensions::create($data);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return in_array(
$type,
[
AuthenticationExtensions::class,
AuthenticationExtensionsClientOutputs::class,
AuthenticationExtensionsClientInputs::class,
],
true
);
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AuthenticationExtensions::class => true,
AuthenticationExtensionsClientInputs::class => true,
AuthenticationExtensionsClientOutputs::class => true,
];
}
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof AuthenticationExtensions);
$extensions = [];
foreach ($data->extensions as $extension) {
$extensions[$extension->name] = $extension->value;
}
return $extensions;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof AuthenticationExtensions;
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\AttestationStatement\AttestationObject;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorData;
use Webauthn\CollectedClientData;
use Webauthn\Util\Base64;
final class AuthenticatorAssertionResponseDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$data['authenticatorData'] = Base64::decode($data['authenticatorData']);
$data['signature'] = Base64::decode($data['signature']);
$data['clientDataJSON'] = Base64UrlSafe::decodeNoPadding($data['clientDataJSON']);
$userHandle = $data['userHandle'] ?? null;
if ($userHandle !== '' && $userHandle !== null) {
$userHandle = Base64::decode($userHandle);
}
return AuthenticatorAssertionResponse::create(
$this->denormalizer->denormalize($data['clientDataJSON'], CollectedClientData::class, $format, $context),
$this->denormalizer->denormalize($data['authenticatorData'], AuthenticatorData::class, $format, $context),
$data['signature'],
$userHandle ?? null,
! isset($data['attestationObject']) ? null : $this->denormalizer->denormalize(
$data['attestationObject'],
AttestationObject::class,
$format,
$context
),
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === AuthenticatorAssertionResponse::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AuthenticatorAssertionResponse::class => true,
];
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\AttestationStatement\AttestationObject;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\CollectedClientData;
use Webauthn\Util\Base64;
final class AuthenticatorAttestationResponseDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$data['clientDataJSON'] = Base64UrlSafe::decodeNoPadding($data['clientDataJSON']);
$data['attestationObject'] = Base64::decode($data['attestationObject']);
$clientDataJSON = $this->denormalizer->denormalize(
$data['clientDataJSON'],
CollectedClientData::class,
$format,
$context
);
$attestationObject = $this->denormalizer->denormalize(
$data['attestationObject'],
AttestationObject::class,
$format,
$context
);
return AuthenticatorAttestationResponse::create(
$clientDataJSON,
$attestationObject,
$data['transports'] ?? [],
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === AuthenticatorAttestationResponse::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AuthenticatorAttestationResponse::class => true,
];
}
}
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use CBOR\ByteStringObject;
use CBOR\Decoder;
use CBOR\ListObject;
use CBOR\MapObject;
use CBOR\NegativeIntegerObject;
use CBOR\TextStringObject;
use CBOR\UnsignedIntegerObject;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Uid\Uuid;
use Webauthn\AttestedCredentialData;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
use Webauthn\AuthenticatorData;
use Webauthn\Exception\InvalidDataException;
use Webauthn\StringStream;
use function chr;
use function ord;
final class AuthenticatorDataDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
private readonly Decoder $decoder;
public function __construct()
{
$this->decoder = Decoder::create();
}
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$authData = $this->fixIncorrectEdDSAKey($data);
$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 === null ? null : $this->denormalizer->denormalize(
$extension,
AuthenticationExtensions::class,
$format,
$context
),
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === AuthenticatorData::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AuthenticatorData::class => true,
];
}
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,45 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorResponse;
use Webauthn\Exception\InvalidDataException;
use function array_key_exists;
final class AuthenticatorResponseDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$realType = match (true) {
array_key_exists('attestationObject', $data) => AuthenticatorAttestationResponse::class,
array_key_exists('signature', $data) => AuthenticatorAssertionResponse::class,
default => throw InvalidDataException::create($data, 'Unable to create the response object'),
};
return $this->denormalizer->denormalize($data, $realType, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === AuthenticatorResponse::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
AuthenticatorResponse::class => true,
];
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\CollectedClientData;
use const JSON_THROW_ON_ERROR;
final class CollectedClientDataDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
return CollectedClientData::create($data, json_decode($data, true, flags: JSON_THROW_ON_ERROR));
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === CollectedClientData::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
CollectedClientData::class => true,
];
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\MetadataService\Statement\ExtensionDescriptor;
use function array_key_exists;
/**
* @final
*/
class ExtensionDescriptorDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
private const ALREADY_CALLED = 'EXTENSION_DESCRIPTOR_PREPROCESS_ALREADY_CALLED';
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (array_key_exists('fail_if_unknown', $data)) {
$data['failIfUnknown'] = $data['fail_if_unknown'];
unset($data['fail_if_unknown']);
}
$context[self::ALREADY_CALLED] = true;
return $this->denormalizer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
if ($context[self::ALREADY_CALLED] ?? false) {
return false;
}
return $type === ExtensionDescriptor::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
ExtensionDescriptor::class => false,
];
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\AuthenticatorResponse;
use Webauthn\Exception\InvalidDataException;
use Webauthn\PublicKeyCredential;
use Webauthn\Util\Base64;
use function array_key_exists;
final class PublicKeyCredentialDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (! array_key_exists('id', $data)) {
return $data;
}
$id = Base64UrlSafe::decodeNoPadding($data['id']);
$rawId = Base64::decode($data['rawId']);
hash_equals($id, $rawId) || throw InvalidDataException::create($data, 'Invalid ID');
$data['rawId'] = $rawId;
return PublicKeyCredential::create(
null,
$data['type'],
$data['rawId'],
$this->denormalizer->denormalize($data['response'], AuthenticatorResponse::class, $format, $context),
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === PublicKeyCredential::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
PublicKeyCredential::class => true,
];
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\PublicKeyCredentialDescriptor;
use function assert;
use function count;
final class PublicKeyCredentialDescriptorNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof PublicKeyCredentialDescriptor);
$result = [
'type' => $data->type,
'id' => Base64UrlSafe::encodeUnpadded($data->id),
];
if (count($data->transports) !== 0) {
$result['transports'] = $data->transports;
}
return $result;
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof PublicKeyCredentialDescriptor;
}
public function getSupportedTypes(?string $format): array
{
return [
PublicKeyCredentialDescriptor::class => true,
];
}
}
@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Exception\BadMethodCallException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialDescriptor;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use function array_key_exists;
use function assert;
use function in_array;
final class PublicKeyCredentialOptionsDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface, NormalizerInterface, NormalizerAwareInterface
{
use DenormalizerAwareTrait;
use NormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (array_key_exists('challenge', $data)) {
$data['challenge'] = Base64UrlSafe::decodeNoPadding($data['challenge']);
}
foreach (['allowCredentials', 'excludeCredentials'] as $key) {
if (array_key_exists($key, $data)) {
foreach ($data[$key] ?? [] as $item => $allowCredential) {
$data[$key][$item]['id'] = Base64UrlSafe::decodeNoPadding($allowCredential['id']);
}
}
}
if ($type === PublicKeyCredentialCreationOptions::class) {
return PublicKeyCredentialCreationOptions::create(
$this->denormalizer->denormalize($data['rp'], PublicKeyCredentialRpEntity::class, $format, $context),
$this->denormalizer->denormalize(
$data['user'],
PublicKeyCredentialUserEntity::class,
$format,
$context
),
$data['challenge'],
! isset($data['pubKeyCredParams']) ? [] : $this->denormalizer->denormalize(
$data['pubKeyCredParams'],
PublicKeyCredentialParameters::class . '[]',
$format,
$context
),
! isset($data['authenticatorSelection']) ? null : $this->denormalizer->denormalize(
$data['authenticatorSelection'],
AuthenticatorSelectionCriteria::class,
$format,
$context
),
$data['attestation'] ?? null,
! isset($data['excludeCredentials']) ? [] : $this->denormalizer->denormalize(
$data['excludeCredentials'],
PublicKeyCredentialDescriptor::class . '[]',
$format,
$context
),
$data['timeout'] ?? null,
! isset($data['extensions']) ? null : $this->denormalizer->denormalize(
$data['extensions'],
AuthenticationExtensions::class,
$format,
$context
),
);
}
if ($type === PublicKeyCredentialRequestOptions::class) {
return PublicKeyCredentialRequestOptions::create(
$data['challenge'],
$data['rpId'] ?? null,
! isset($data['allowCredentials']) ? [] : $this->denormalizer->denormalize(
$data['allowCredentials'],
PublicKeyCredentialDescriptor::class . '[]',
$format,
$context
),
$data['userVerification'] ?? null,
$data['timeout'] ?? null,
! isset($data['extensions']) ? null : $this->denormalizer->denormalize(
$data['extensions'],
AuthenticationExtensions::class,
$format,
$context
),
);
}
throw new BadMethodCallException('Unsupported type');
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return in_array(
$type,
[PublicKeyCredentialCreationOptions::class, PublicKeyCredentialRequestOptions::class],
true
);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof PublicKeyCredentialCreationOptions || $data instanceof PublicKeyCredentialRequestOptions;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
PublicKeyCredentialCreationOptions::class => true,
PublicKeyCredentialRequestOptions::class => true,
];
}
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert(
$data instanceof PublicKeyCredentialCreationOptions || $data instanceof PublicKeyCredentialRequestOptions
);
$json = [
'challenge' => Base64UrlSafe::encodeUnpadded($data->challenge),
'timeout' => $data->timeout,
'extensions' => $data->extensions->count() === 0 ? null : $this->normalizer->normalize(
$data->extensions,
$format,
$context
),
];
if ($data instanceof PublicKeyCredentialCreationOptions) {
$json = [
...$json,
'rp' => $this->normalizer->normalize($data->rp, $format, $context),
'user' => $this->normalizer->normalize($data->user, $format, $context),
'pubKeyCredParams' => $this->normalizer->normalize(
$data->pubKeyCredParams,
PublicKeyCredentialParameters::class . '[]',
$context
),
'authenticatorSelection' => $data->authenticatorSelection === null ? null : $this->normalizer->normalize(
$data->authenticatorSelection,
$format,
$context
),
'attestation' => $data->attestation,
'excludeCredentials' => $this->normalizer->normalize($data->excludeCredentials, $format, $context),
];
}
if ($data instanceof PublicKeyCredentialRequestOptions) {
$json = [
...$json,
'rpId' => $data->rpId,
'allowCredentials' => $this->normalizer->normalize($data->allowCredentials, $format, $context),
'userVerification' => $data->userVerification,
];
}
return array_filter($json, static fn ($value) => $value !== null && $value !== []);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Webauthn\Exception\InvalidDataException;
use Webauthn\PublicKeyCredentialParameters;
use function array_key_exists;
final class PublicKeyCredentialParametersDenormalizer implements DenormalizerInterface
{
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (! array_key_exists('type', $data) || ! array_key_exists('alg', $data)) {
throw new InvalidDataException($data, 'Missing type or alg');
}
return PublicKeyCredentialParameters::create($data['type'], $data['alg']);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === PublicKeyCredentialParameters::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
PublicKeyCredentialParameters::class => true,
];
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Uid\Uuid;
use Webauthn\Exception\InvalidDataException;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\TrustPath\TrustPath;
use Webauthn\Util\Base64;
use function array_key_exists;
use function assert;
final class PublicKeyCredentialSourceDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface, NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
$keys = ['publicKeyCredentialId', 'credentialPublicKey', 'userHandle'];
foreach ($keys as $key) {
array_key_exists($key, $data) || throw InvalidDataException::create($data, 'Missing ' . $key);
$data[$key] = Base64::decode($data[$key]);
}
return PublicKeyCredentialSource::create(
$data['publicKeyCredentialId'],
$data['type'],
$data['transports'],
$data['attestationType'],
$this->denormalizer->denormalize($data['trustPath'], TrustPath::class, $format, $context),
Uuid::fromString($data['aaguid']),
$data['credentialPublicKey'],
$data['userHandle'],
$data['counter'],
$data['otherUI'] ?? null,
$data['backupEligible'] ?? null,
$data['backupStatus'] ?? null,
$data['uvInitialized'] ?? null,
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === PublicKeyCredentialSource::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
PublicKeyCredentialSource::class => true,
];
}
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof PublicKeyCredentialSource);
$result = [
'publicKeyCredentialId' => Base64UrlSafe::encodeUnpadded($data->publicKeyCredentialId),
'type' => $data->type,
'transports' => $data->transports,
'attestationType' => $data->attestationType,
'trustPath' => $this->normalizer->normalize($data->trustPath, $format, $context),
'aaguid' => $this->normalizer->normalize($data->aaguid, $format, $context),
'credentialPublicKey' => Base64UrlSafe::encodeUnpadded($data->credentialPublicKey),
'userHandle' => Base64UrlSafe::encodeUnpadded($data->userHandle),
'counter' => $data->counter,
'otherUI' => $data->otherUI,
'backupEligible' => $data->backupEligible,
'backupStatus' => $data->backupStatus,
'uvInitialized' => $data->uvInitialized,
];
return array_filter($result, static fn ($value): bool => $value !== null);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof PublicKeyCredentialSource;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\Util\Base64;
use function array_key_exists;
use function assert;
final class PublicKeyCredentialUserEntityDenormalizer implements DenormalizerInterface, NormalizerInterface
{
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
if (! array_key_exists('id', $data)) {
return $data;
}
$data['id'] = Base64::decode($data['id']);
return PublicKeyCredentialUserEntity::create(
$data['name'],
$data['id'],
$data['displayName'],
$data['icon'] ?? null
);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === PublicKeyCredentialUserEntity::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
PublicKeyCredentialUserEntity::class => true,
];
}
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof PublicKeyCredentialUserEntity);
$normalized = [
'id' => Base64UrlSafe::encodeUnpadded($data->id),
'name' => $data->name,
'displayName' => $data->displayName,
'icon' => $data->icon,
];
return array_filter($normalized, fn ($value) => $value !== null);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof PublicKeyCredentialUserEntity;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\Exception\InvalidTrustPathException;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
use Webauthn\TrustPath\TrustPath;
use function array_key_exists;
use function assert;
final class TrustPathDenormalizer implements DenormalizerInterface, NormalizerInterface
{
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed
{
return match (true) {
array_key_exists('ecdaaKeyId', $data) => new EcdaaKeyIdTrustPath($data),
array_key_exists('x5c', $data) => CertificateTrustPath::create($data),
$data === [], isset($data['type']) && $data['type'] === EmptyTrustPath::class => EmptyTrustPath::create(),
default => throw new InvalidTrustPathException('Unsupported trust path type'),
};
}
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
{
return $type === TrustPath::class;
}
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
TrustPath::class => true,
];
}
/**
* @return array<string, mixed>
*/
public function normalize(mixed $data, ?string $format = null, array $context = []): array
{
assert($data instanceof TrustPath);
return match (true) {
$data instanceof EcdaaKeyIdTrustPath => [
'ecdaaKeyId' => $data->getEcdaaKeyId(),
],
$data instanceof CertificateTrustPath => [
'x5c' => $data->certificates,
],
$data instanceof EmptyTrustPath => [],
default => throw new InvalidTrustPathException('Unsupported trust path type'),
};
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof TrustPath;
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Webauthn\MetadataService\Statement\VerificationMethodANDCombinations;
use Webauthn\MetadataService\Statement\VerificationMethodDescriptor;
use function assert;
final class VerificationMethodANDCombinationsDenormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
/**
* @return array<class-string, bool>
*/
public function getSupportedTypes(?string $format): array
{
return [
VerificationMethodANDCombinations::class => true,
];
}
/**
* @return array<VerificationMethodDescriptor>
*/
public function normalize(mixed $object, ?string $format = null, array $context = []): array
{
assert($object instanceof VerificationMethodANDCombinations);
return array_map(
fn ($verificationMethod) => $this->normalizer->normalize($verificationMethod, $format, $context),
$object->verificationMethods
);
}
public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
{
return $data instanceof VerificationMethodANDCombinations;
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Webauthn\Denormalizer;
use RuntimeException;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\UidNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
final class WebauthnSerializerFactory
{
private const PACKAGE_SYMFONY_PROPERTY_INFO = 'symfony/property-info';
private const PACKAGE_SYMFONY_SERIALIZER = 'symfony/serializer';
private const PACKAGE_PHPDOCUMENTOR_REFLECTION_DOCBLOCK = 'phpdocumentor/reflection-docblock';
public function __construct(
private readonly AttestationStatementSupportManager $attestationStatementSupportManager
) {
}
public function create(): SerializerInterface
{
foreach (self::getRequiredSerializerClasses() as $class => $package) {
if (! class_exists($class)) {
throw new RuntimeException(sprintf(
'The class "%s" is required. Please install the package "%s" to use this feature.',
$class,
$package
));
}
}
$denormalizers = [
new ExtensionDescriptorDenormalizer(),
new VerificationMethodANDCombinationsDenormalizer(),
new AuthenticationExtensionNormalizer(),
new PublicKeyCredentialDescriptorNormalizer(),
new AttestedCredentialDataNormalizer(),
new AttestationObjectDenormalizer(),
new AttestationStatementDenormalizer($this->attestationStatementSupportManager),
new AuthenticationExtensionsDenormalizer(),
new AuthenticatorAssertionResponseDenormalizer(),
new AuthenticatorAttestationResponseDenormalizer(),
new AuthenticatorDataDenormalizer(),
new AuthenticatorResponseDenormalizer(),
new CollectedClientDataDenormalizer(),
new PublicKeyCredentialDenormalizer(),
new PublicKeyCredentialOptionsDenormalizer(),
new PublicKeyCredentialSourceDenormalizer(),
new PublicKeyCredentialUserEntityDenormalizer(),
new TrustPathDenormalizer(),
new UidNormalizer(),
new ArrayDenormalizer(),
new ObjectNormalizer(
propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [
new PhpDocExtractor(),
new ReflectionExtractor(),
])
),
];
return new Serializer($denormalizers, [new JsonEncoder()]);
}
/**
* @return array<class-string, string>
*/
private static function getRequiredSerializerClasses(): array
{
return [
UidNormalizer::class => self::PACKAGE_SYMFONY_SERIALIZER,
ArrayDenormalizer::class => self::PACKAGE_SYMFONY_SERIALIZER,
ObjectNormalizer::class => self::PACKAGE_SYMFONY_SERIALIZER,
PropertyInfoExtractor::class => self::PACKAGE_SYMFONY_PROPERTY_INFO,
PhpDocExtractor::class => self::PACKAGE_PHPDOCUMENTOR_REFLECTION_DOCBLOCK,
ReflectionExtractor::class => self::PACKAGE_SYMFONY_PROPERTY_INFO,
JsonEncoder::class => self::PACKAGE_SYMFONY_SERIALIZER,
Serializer::class => self::PACKAGE_SYMFONY_SERIALIZER,
];
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Webauthn\AttestationStatement\AttestationObject;
class AttestationObjectLoaded implements WebauthnEvent
{
public function __construct(
public readonly AttestationObject $attestationObject
) {
}
public static function create(AttestationObject $attestationObject): self
{
return new self($attestationObject);
}
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Webauthn\AttestationStatement\AttestationStatement;
class AttestationStatementLoaded implements WebauthnEvent
{
public function __construct(
public readonly AttestationStatement $attestationStatement
) {
}
public static function create(AttestationStatement $attestationStatement): self
{
return new self($attestationStatement);
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
class AuthenticatorAssertionResponseValidationFailedEvent
{
public function __construct(
public readonly string|PublicKeyCredentialSource $credentialId,
public readonly AuthenticatorAssertionResponse $authenticatorAssertionResponse,
public readonly PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
public readonly ServerRequestInterface|string $host,
public readonly ?string $userHandle,
public readonly Throwable $throwable
) {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to 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 (! $this->credentialId instanceof PublicKeyCredentialSource) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.6.0',
'Passing a string for the argument "$credentialId" is deprecated since 4.6.0. Please set the PublicKeyCredentialSource instead.'
);
}
}
/**
* @deprecated since 4.7.0 and will be removed in 5.0.0. Please use the `getCredential()` method instead
* @infection-ignore-all
*/
public function getCredentialId(): string
{
return $this->credentialId instanceof PublicKeyCredentialSource ? $this->credentialId->publicKeyCredentialId : $this->credentialId;
}
public function getCredential(): ?PublicKeyCredentialSource
{
return $this->credentialId instanceof PublicKeyCredentialSource ? $this->credentialId : null;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getAuthenticatorAssertionResponse(): AuthenticatorAssertionResponse
{
return $this->authenticatorAssertionResponse;
}
public function getPublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions
{
return $this->publicKeyCredentialRequestOptions;
}
/**
* @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead
* @infection-ignore-all
*/
public function getRequest(): ServerRequestInterface|string
{
return $this->host;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getUserHandle(): ?string
{
return $this->userHandle;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getThrowable(): Throwable
{
return $this->throwable;
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Psr\Http\Message\ServerRequestInterface;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
class AuthenticatorAssertionResponseValidationSucceededEvent
{
public function __construct(
public readonly null|string $credentialId,
public readonly AuthenticatorAssertionResponse $authenticatorAssertionResponse,
public readonly PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
public readonly ServerRequestInterface|string $host,
public readonly ?string $userHandle,
public readonly PublicKeyCredentialSource $publicKeyCredentialSource
) {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to 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 ($this->credentialId !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.6.0',
'The argument "$credentialId" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set null instead.'
);
}
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getCredentialId(): string
{
return $this->publicKeyCredentialSource->publicKeyCredentialId;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getAuthenticatorAssertionResponse(): AuthenticatorAssertionResponse
{
return $this->authenticatorAssertionResponse;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getPublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions
{
return $this->publicKeyCredentialRequestOptions;
}
/**
* @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead
* @infection-ignore-all
*/
public function getRequest(): ServerRequestInterface|string
{
return $this->host;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getUserHandle(): ?string
{
return $this->userHandle;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getPublicKeyCredentialSource(): PublicKeyCredentialSource
{
return $this->publicKeyCredentialSource;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialCreationOptions;
class AuthenticatorAttestationResponseValidationFailedEvent
{
public function __construct(
public readonly AuthenticatorAttestationResponse $authenticatorAttestationResponse,
public readonly PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
public readonly ServerRequestInterface|string $host,
public readonly Throwable $throwable
) {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to 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
)
);
}
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getAuthenticatorAttestationResponse(): AuthenticatorAttestationResponse
{
return $this->authenticatorAttestationResponse;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getPublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions
{
return $this->publicKeyCredentialCreationOptions;
}
/**
* @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead
* @infection-ignore-all
*/
public function getRequest(): ServerRequestInterface|string
{
return $this->host;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getThrowable(): Throwable
{
return $this->throwable;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Psr\Http\Message\ServerRequestInterface;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialSource;
class AuthenticatorAttestationResponseValidationSucceededEvent
{
public function __construct(
public readonly AuthenticatorAttestationResponse $authenticatorAttestationResponse,
public readonly PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
public readonly ServerRequestInterface|string $host,
public readonly PublicKeyCredentialSource $publicKeyCredentialSource
) {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to 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
)
);
}
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getAuthenticatorAttestationResponse(): AuthenticatorAttestationResponse
{
return $this->authenticatorAttestationResponse;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getPublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions
{
return $this->publicKeyCredentialCreationOptions;
}
/**
* @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead
* @infection-ignore-all
*/
public function getRequest(): ServerRequestInterface|string
{
return $this->host;
}
/**
* @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead.
*/
public function getPublicKeyCredentialSource(): PublicKeyCredentialSource
{
return $this->publicKeyCredentialSource;
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
/**
* @final
*/
class BeforeCertificateChainValidation implements WebauthnEvent
{
/**
* @param string[] $untrustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly string $trustedCertificate
) {
}
/**
* @param string[] $untrustedCertificates
*/
public static function create(array $untrustedCertificates, string $trustedCertificate): self
{
return new self($untrustedCertificates, $trustedCertificate);
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Psr\EventDispatcher\EventDispatcherInterface;
interface CanDispatchEvents
{
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void;
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
/**
* @final
*/
class CertificateChainValidationFailed implements WebauthnEvent
{
/**
* @param string[] $untrustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly string $trustedCertificate
) {
}
/**
* @param string[] $untrustedCertificates
*/
public static function create(array $untrustedCertificates, string $trustedCertificate): self
{
return new self($untrustedCertificates, $trustedCertificate);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
/**
* @final
*/
class CertificateChainValidationSucceeded implements WebauthnEvent
{
/**
* @param string[] $untrustedCertificates
*/
public function __construct(
public readonly array $untrustedCertificates,
public readonly string $trustedCertificate
) {
}
/**
* @param string[] $untrustedCertificates
*/
public static function create(array $untrustedCertificates, string $trustedCertificate): self
{
return new self($untrustedCertificates, $trustedCertificate);
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Webauthn\MetadataService\Statement\MetadataStatement;
/**
* @final
*/
class MetadataStatementFound implements WebauthnEvent
{
public function __construct(
public readonly MetadataStatement $metadataStatement
) {
}
public static function create(MetadataStatement $metadataStatement): self
{
return new self($metadataStatement);
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* @final
*/
class NullEventDispatcher implements EventDispatcherInterface
{
public function dispatch(object $event): object
{
return $event;
}
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Webauthn\Event;
interface WebauthnEvent
{
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Webauthn\Exception;
class AttestationStatementException extends WebauthnException
{
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Webauthn\Exception;
use Throwable;
final class AttestationStatementLoadingException extends AttestationStatementException
{
/**
* @param array<string, mixed> $attestation
*/
public function __construct(
public readonly array $attestation,
string $message,
?Throwable $previous = null
) {
parent::__construct($message, $previous);
}
/**
* @param array<string, mixed> $attestation
*/
public static function create(
array $attestation,
string $message = 'Invalid attestation object',
?Throwable $previous = null
): self {
return new self($attestation, $message, $previous);
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\Exception;
use Throwable;
final class AttestationStatementVerificationException extends AttestationStatementException
{
public static function create(string $message = 'Invalid attestation object', ?Throwable $previous = null): self
{
return new self($message, $previous);
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\Exception;
use Throwable;
final class AuthenticationExtensionException extends WebauthnException
{
public static function create(string $message, ?Throwable $previous = null): self
{
return new self($message, $previous);
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\Exception;
use Throwable;
class AuthenticatorResponseVerificationException extends WebauthnException
{
public static function create(string $message, ?Throwable $previous = null): self
{
return new self($message, $previous);
}
}

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