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

This commit is contained in:
root
2026-03-16 08:42:57 +00:00
parent b8905de237
commit f390426546
3354 changed files with 505213 additions and 3 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018-2022 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use CBOR\Decoder;
use CBOR\Normalizable;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use Psr\EventDispatcher\EventDispatcherInterface;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\Exception\AttestationStatementVerificationException;
use Webauthn\Exception\InvalidAttestationStatementException;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use function array_key_exists;
use function count;
use function is_array;
use function openssl_pkey_get_public;
use function openssl_verify;
final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private readonly Decoder $decoder;
private EventDispatcherInterface $dispatcher;
public function __construct()
{
$this->decoder = Decoder::create();
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'android-key';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create($attestation);
foreach (['sig', 'x5c', 'alg'] as $key) {
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
sprintf('The attestation statement value "%s" is missing.', $key)
);
}
$certificates = $attestation['attStmt']['x5c'];
(is_countable($certificates) ? count(
$certificates
) : 0) > 0 || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "x5c" must be a list with at least one certificate.'
);
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
$attestationStatement = AttestationStatement::createBasic(
$attestation['fmt'],
$attestation['attStmt'],
CertificateTrustPath::create($certificates)
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$trustPath = $attestationStatement->trustPath;
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid trust path. Shall contain certificates.'
);
$certificates = $trustPath->certificates;
//Decode leaf attestation certificate
$leaf = $certificates[0];
$this->checkCertificate($leaf, $clientDataJSONHash, $authenticatorData);
$signedData = $authenticatorData->authData . $clientDataJSONHash;
$alg = $attestationStatement->get('alg');
return openssl_verify(
$signedData,
$attestationStatement->get('sig'),
$leaf,
Algorithms::getOpensslAlgorithmFor((int) $alg)
) === 1;
}
private function checkCertificate(
string $certificate,
string $clientDataHash,
AuthenticatorData $authenticatorData
): void {
$resource = openssl_pkey_get_public($certificate);
$details = openssl_pkey_get_details($resource);
is_array($details) || throw AttestationStatementVerificationException::create(
'Unable to read the certificate'
);
//Check that authData publicKey matches the public key in the attestation certificate
$attestedCredentialData = $authenticatorData->attestedCredentialData;
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
'No attested credential data found'
);
$publicKeyData = $attestedCredentialData->credentialPublicKey;
$publicKeyData !== null || throw AttestationStatementVerificationException::create(
'No attested public key found'
);
$publicDataStream = new StringStream($publicKeyData);
$coseKey = $this->decoder->decode($publicDataStream);
$coseKey instanceof Normalizable || throw AttestationStatementVerificationException::create(
'Invalid attested public key found'
);
$publicDataStream->isEOF() || throw AttestationStatementVerificationException::create(
'Invalid public key data. Presence of extra bytes.'
);
$publicDataStream->close();
$publicKey = Key::createFromData($coseKey->normalize());
($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create(
'Unsupported key type'
);
$publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create(
'Invalid key'
);
/*---------------------------*/
$certDetails = openssl_x509_parse($certificate);
//Find Android KeyStore Extension with OID "1.3.6.1.4.1.11129.2.1.17" in certificate extensions
is_array(
$certDetails
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
'The certificate has no extension'
);
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
'The certificate has no extension'
);
array_key_exists(
'1.3.6.1.4.1.11129.2.1.17',
$certDetails['extensions']
) || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing'
);
$extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17'];
$extensionAsAsn1 = Sequence::fromDER($extension);
$extensionAsAsn1->has(4);
//Check that attestationChallenge is set to the clientDataHash.
$extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
);
$ext = $extensionAsAsn1->at(4)
->asElement();
$ext instanceof OctetString || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
);
$clientDataHash === $ext->string() || throw AttestationStatementVerificationException::create(
'The client data hash is not valid'
);
//Check that both teeEnforced and softwareEnforced structures don't contain allApplications(600) tag.
$extensionAsAsn1->has(6) || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
);
$softwareEnforcedFlags = $extensionAsAsn1->at(6)
->asElement();
$softwareEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
);
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
$extensionAsAsn1->has(7) || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
);
$teeEnforcedFlags = $extensionAsAsn1->at(7)
->asElement();
$teeEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
);
$this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags);
}
private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void
{
foreach ($sequence->elements() as $tag) {
$tag->asElement() instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
'Invalid tag'
);
$tag->asElement()
->tag() !== 600 || throw AttestationStatementVerificationException::create('Forbidden tag 600 found');
}
}
}
@@ -0,0 +1,382 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use Jose\Component\Core\Algorithm as AlgorithmInterface;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\EdDSA;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Algorithm\ES384;
use Jose\Component\Signature\Algorithm\ES512;
use Jose\Component\Signature\Algorithm\PS256;
use Jose\Component\Signature\Algorithm\PS384;
use Jose\Component\Signature\Algorithm\PS512;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\Algorithm\RS384;
use Jose\Component\Signature\Algorithm\RS512;
use Jose\Component\Signature\JWS;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Psr\Clock\ClockInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\Exception\AttestationStatementVerificationException;
use Webauthn\Exception\InvalidAttestationStatementException;
use Webauthn\Exception\UnsupportedFeatureException;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\TrustPath\CertificateTrustPath;
use function array_key_exists;
use function count;
use function is_array;
use function is_int;
use function is_string;
use const JSON_THROW_ON_ERROR;
/**
* @deprecated since 4.9.0 and will be removed in 5.0.0. Android SafetyNet is now deprecated.
*/
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private ?string $apiKey = null;
private null|ClientInterface|HttpClientInterface $client = null;
private readonly CompactSerializer $jwsSerializer;
private ?JWSVerifier $jwsVerifier = null;
private ?RequestFactoryInterface $requestFactory = null;
private int $leeway = 0;
private int $maxAge = 60000;
private EventDispatcherInterface $dispatcher;
public function __construct(
private readonly null|ClockInterface $clock = null
) {
if ($this->clock === null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$clock" will be required in 5.0.0. Please set a clock instance.'
);
}
if (! class_exists(RS256::class) || ! class_exists(JWKFactory::class)) {
throw UnsupportedFeatureException::create(
'The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-library?'
);
}
$this->jwsSerializer = new CompactSerializer();
$this->initJwsVerifier();
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(null|ClockInterface $clock = null): self
{
return new self($clock);
}
public function enableApiVerification(
ClientInterface|HttpClientInterface $client,
string $apiKey,
?RequestFactoryInterface $requestFactory = null
): self {
$this->apiKey = $apiKey;
$this->client = $client;
$this->requestFactory = $requestFactory;
if ($requestFactory !== null && ! $client instanceof HttpClientInterface) {
trigger_deprecation(
'web-auth/metadata-service',
'4.7.0',
'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$client" argument.'
);
}
return $this;
}
public function setMaxAge(int $maxAge): self
{
$this->maxAge = $maxAge;
return $this;
}
public function setLeeway(int $leeway): self
{
$this->leeway = $leeway;
return $this;
}
public function name(): string
{
return 'android-safetynet';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
$attestation
);
foreach (['ver', 'response'] as $key) {
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
sprintf('The attestation statement value "%s" is missing.', $key)
);
$attestation['attStmt'][$key] !== '' || throw AttestationStatementLoadingException::create(
$attestation,
sprintf('The attestation statement value "%s" is empty.', $key)
);
}
$jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
$jwsHeader = $jws->getSignature(0)
->getProtectedHeader();
array_key_exists('x5c', $jwsHeader) || throw AttestationStatementLoadingException::create(
$attestation,
'The response in the attestation statement must contain a "x5c" header.'
);
(is_countable($jwsHeader['x5c']) ? count(
$jwsHeader['x5c']
) : 0) > 0 || throw AttestationStatementLoadingException::create(
$attestation,
'The "x5c" parameter in the attestation statement response must contain at least one certificate.'
);
$certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
$attestation['attStmt']['jws'] = $jws;
$attestationStatement = AttestationStatement::createBasic(
$this->name(),
$attestation['attStmt'],
CertificateTrustPath::create($certificates)
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$trustPath = $attestationStatement->trustPath;
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid trust path'
);
$certificates = $trustPath->certificates;
$firstCertificate = current($certificates);
is_string($firstCertificate) || throw InvalidAttestationStatementException::create(
$attestationStatement,
'No certificate'
);
$parsedCertificate = openssl_x509_parse($firstCertificate);
is_array($parsedCertificate) || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid attestation object'
);
array_key_exists('subject', $parsedCertificate) || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid attestation object'
);
array_key_exists('CN', $parsedCertificate['subject']) || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid attestation object'
);
$parsedCertificate['subject']['CN'] === 'attest.android.com' || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid attestation object'
);
/** @var JWS $jws */
$jws = $attestationStatement->get('jws');
$payload = $jws->getPayload();
$this->validatePayload($payload, $clientDataJSONHash, $authenticatorData);
//Check the signature
$this->validateSignature($jws, $trustPath);
//Check against Google service
$this->validateUsingGoogleApi($attestationStatement);
return true;
}
private function validatePayload(
?string $payload,
string $clientDataJSONHash,
AuthenticatorData $authenticatorData
): void {
$payload !== null || throw AttestationStatementVerificationException::create('Invalid attestation object');
$payload = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
array_key_exists('nonce', $payload) || throw AttestationStatementVerificationException::create(
'Invalid attestation object. "nonce" is missing.'
);
$payload['nonce'] === base64_encode(
hash('sha256', $authenticatorData->authData . $clientDataJSONHash, true)
) || throw AttestationStatementVerificationException::create('Invalid attestation object. Invalid nonce');
array_key_exists('ctsProfileMatch', $payload) || throw AttestationStatementVerificationException::create(
'Invalid attestation object. "ctsProfileMatch" is missing.'
);
$payload['ctsProfileMatch'] || throw AttestationStatementVerificationException::create(
'Invalid attestation object. "ctsProfileMatch" value is false.'
);
array_key_exists('timestampMs', $payload) || throw AttestationStatementVerificationException::create(
'Invalid attestation object. Timestamp is missing.'
);
is_int($payload['timestampMs']) || throw AttestationStatementVerificationException::create(
'Invalid attestation object. Timestamp shall be an integer.'
);
$currentTime = ($this->clock?->now()->getTimestamp() ?? time()) * 1000;
$payload['timestampMs'] <= $currentTime + $this->leeway || throw AttestationStatementVerificationException::create(
sprintf(
'Invalid attestation object. Issued in the future. Current time: %d. Response time: %d',
$currentTime,
$payload['timestampMs']
)
);
$currentTime - $payload['timestampMs'] <= $this->maxAge || throw AttestationStatementVerificationException::create(
sprintf(
'Invalid attestation object. Too old. Current time: %d. Response time: %d',
$currentTime,
$payload['timestampMs']
)
);
}
private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void
{
$jwk = JWKFactory::createFromCertificate($trustPath->certificates[0]);
$isValid = $this->jwsVerifier?->verifyWithKey($jws, $jwk, 0);
$isValid === true || throw AttestationStatementVerificationException::create('Invalid response signature');
}
private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
{
if ($this->client === null || $this->apiKey === null) {
return;
}
$uri = sprintf(
'https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s',
urlencode($this->apiKey)
);
$requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
if ($this->client instanceof HttpClientInterface) {
$responseBody = $this->validateUsingGoogleApiWithSymfonyClient($requestBody, $uri);
} else {
$responseBody = $this->validateUsingGoogleApiWithPsrClient($requestBody, $uri);
}
$responseBodyJson = json_decode($responseBody, true, flags: JSON_THROW_ON_ERROR);
array_key_exists(
'isValidSignature',
$responseBodyJson
) || throw AttestationStatementVerificationException::create('Invalid response.');
$responseBodyJson['isValidSignature'] === true || throw AttestationStatementVerificationException::create(
'Invalid response.'
);
}
private function getResponseBody(ResponseInterface $response): string
{
$responseBody = '';
$response->getBody()
->rewind();
do {
$tmp = $response->getBody()
->read(1024);
if ($tmp === '') {
break;
}
$responseBody .= $tmp;
} while (true);
return $responseBody;
}
/**
* @param string[] $certificates
*
* @return string[]
*/
private function convertCertificatesToPem(array $certificates): array
{
foreach ($certificates as $k => $v) {
$certificates[$k] = CertificateToolbox::fixPEMStructure($v);
}
return $certificates;
}
private function initJwsVerifier(): void
{
$algorithmClasses = [
RS256::class, RS384::class, RS512::class,
PS256::class, PS384::class, PS512::class,
ES256::class, ES384::class, ES512::class,
EdDSA::class,
];
/** @var AlgorithmInterface[] $algorithms */
$algorithms = [];
foreach ($algorithmClasses as $algorithm) {
if (class_exists($algorithm)) {
$algorithms[] = new $algorithm();
}
}
$algorithmManager = new AlgorithmManager($algorithms);
$this->jwsVerifier = new JWSVerifier($algorithmManager);
}
private function validateUsingGoogleApiWithSymfonyClient(string $requestBody, string $uri): string
{
$response = $this->client->request('POST', $uri, [
'headers' => [
'content-type' => 'application/json',
],
'body' => $requestBody,
]);
$response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create(
'Request did not succeeded'
);
return $response->getContent();
}
private function validateUsingGoogleApiWithPsrClient(string $requestBody, string $uri): string
{
$request = $this->requestFactory->createRequest('POST', $uri);
$request = $request->withHeader('content-type', 'application/json');
$request->getBody()
->write($requestBody);
$response = $this->client->sendRequest($request);
$response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create(
'Request did not succeeded'
);
return $this->getResponseBody($response);
}
}
@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use CBOR\Decoder;
use CBOR\Normalizable;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\RsaKey;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\Exception\AttestationStatementVerificationException;
use Webauthn\Exception\InvalidAttestationStatementException;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use function array_key_exists;
use function count;
use function is_array;
use function openssl_pkey_get_public;
final class AppleAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private readonly Decoder $decoder;
private EventDispatcherInterface $dispatcher;
public function __construct()
{
$this->decoder = Decoder::create();
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'apple';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
array_key_exists('x5c', $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "x5c" is missing.'
);
$certificates = $attestation['attStmt']['x5c'];
(is_countable($certificates) ? count(
$certificates
) : 0) > 0 || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "x5c" must be a list with at least one certificate.'
);
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
$attestationStatement = AttestationStatement::createAnonymizationCA(
$attestation['fmt'],
$attestation['attStmt'],
CertificateTrustPath::create($certificates)
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$trustPath = $attestationStatement->trustPath;
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid trust path'
);
$certificates = $trustPath->certificates;
//Decode leaf attestation certificate
$leaf = $certificates[0];
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
return true;
}
private function checkCertificateAndGetPublicKey(
string $certificate,
string $clientDataHash,
AuthenticatorData $authenticatorData
): void {
$resource = openssl_pkey_get_public($certificate);
$details = openssl_pkey_get_details($resource);
is_array($details) || throw AttestationStatementVerificationException::create(
'Unable to read the certificate'
);
//Check that authData publicKey matches the public key in the attestation certificate
$attestedCredentialData = $authenticatorData->attestedCredentialData;
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
'No attested credential data found'
);
$publicKeyData = $attestedCredentialData->credentialPublicKey;
$publicKeyData !== null || throw AttestationStatementVerificationException::create(
'No attested public key found'
);
$publicDataStream = new StringStream($publicKeyData);
$coseKey = $this->decoder->decode($publicDataStream);
$coseKey instanceof Normalizable || throw AttestationStatementVerificationException::create(
'Invalid attested public key found'
);
$publicDataStream->isEOF() || throw AttestationStatementVerificationException::create(
'Invalid public key data. Presence of extra bytes.'
);
$publicDataStream->close();
$publicKey = Key::createFromData($coseKey->normalize());
($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create(
'Unsupported key type'
);
//We check the attested key corresponds to the key in the certificate
$publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create(
'Invalid key'
);
/*---------------------------*/
$certDetails = openssl_x509_parse($certificate);
//Find Apple Extension with OID "1.2.840.113635.100.8.2" in certificate extensions
is_array(
$certDetails
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
'The certificate has no extension'
);
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
'The certificate has no extension'
);
array_key_exists(
'1.2.840.113635.100.8.2',
$certDetails['extensions']
) || throw AttestationStatementVerificationException::create(
'The certificate extension "1.2.840.113635.100.8.2" is missing'
);
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
$nonceToHash = $authenticatorData->authData . $clientDataHash;
$nonce = hash('sha256', $nonceToHash);
//'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object
'3024a1220420' . $nonce === bin2hex(
(string) $extension
) || throw AttestationStatementVerificationException::create('The client data hash is not valid');
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use Webauthn\AuthenticatorData;
use Webauthn\MetadataService\Statement\MetadataStatement;
class AttestationObject
{
public ?MetadataStatement $metadataStatement = null;
public function __construct(
public readonly string $rawAttestationObject,
public AttestationStatement $attStmt,
public readonly AuthenticatorData $authData
) {
}
public static function create(
string $rawAttestationObject,
AttestationStatement $attStmt,
AuthenticatorData $authData
): self {
return new self($rawAttestationObject, $attStmt, $authData);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getRawAttestationObject(): string
{
return $this->rawAttestationObject;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAttStmt(): AttestationStatement
{
return $this->attStmt;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function setAttStmt(AttestationStatement $attStmt): void
{
$this->attStmt = $attStmt;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAuthData(): AuthenticatorData
{
return $this->authData;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getMetadataStatement(): ?MetadataStatement
{
return $this->metadataStatement;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function setMetadataStatement(MetadataStatement $metadataStatement): self
{
$this->metadataStatement = $metadataStatement;
return $this;
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use CBOR\Decoder;
use CBOR\Normalizable;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Throwable;
use Webauthn\AuthenticatorDataLoader;
use Webauthn\Event\AttestationObjectLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\InvalidDataException;
use Webauthn\MetadataService\CanLogData;
use Webauthn\StringStream;
use Webauthn\Util\Base64;
use function array_key_exists;
use function is_array;
class AttestationObjectLoader implements CanDispatchEvents, CanLogData
{
private LoggerInterface $logger;
private EventDispatcherInterface $dispatcher;
public function __construct(
private readonly AttestationStatementSupportManager $attestationStatementSupportManager
) {
$this->logger = new NullLogger();
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(AttestationStatementSupportManager $attestationStatementSupportManager): self
{
return new self($attestationStatementSupportManager);
}
public function load(string $data): AttestationObject
{
try {
$this->logger->info('Trying to load the data', [
'data' => $data,
]);
$decodedData = Base64::decode($data);
$stream = new StringStream($decodedData);
$parsed = Decoder::create()->decode($stream);
$this->logger->info('Loading the Attestation Statement');
$parsed instanceof Normalizable || throw InvalidDataException::create(
$parsed,
'Invalid attestation object. Unexpected object.'
);
$attestationObject = $parsed->normalize();
$stream->isEOF() || throw InvalidDataException::create(
null,
'Invalid attestation object. Presence of extra bytes.'
);
$stream->close();
is_array($attestationObject) || throw InvalidDataException::create(
$attestationObject,
'Invalid attestation object'
);
array_key_exists('authData', $attestationObject) || throw InvalidDataException::create(
$attestationObject,
'Invalid attestation object'
);
array_key_exists('fmt', $attestationObject) || throw InvalidDataException::create(
$attestationObject,
'Invalid attestation object'
);
array_key_exists('attStmt', $attestationObject) || throw InvalidDataException::create(
$attestationObject,
'Invalid attestation object'
);
$attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']);
$attestationStatement = $attestationStatementSupport->load($attestationObject);
$this->logger->info('Attestation Statement loaded');
$this->logger->debug('Attestation Statement loaded', [
'attestationStatement' => $attestationStatement,
]);
$authData = $attestationObject['authData'];
$authDataLoader = AuthenticatorDataLoader::create();
$authenticatorData = $authDataLoader->load($authData);
$attestationObject = AttestationObject::create($data, $attestationStatement, $authenticatorData);
$this->logger->info('Attestation Object loaded');
$this->logger->debug('Attestation Object', [
'ed' => $attestationObject,
]);
$this->dispatcher->dispatch(AttestationObjectLoaded::create($attestationObject));
return $attestationObject;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use JsonSerializable;
use Webauthn\Exception\InvalidDataException;
use Webauthn\TrustPath\TrustPath;
use Webauthn\TrustPath\TrustPathLoader;
use function array_key_exists;
class AttestationStatement implements JsonSerializable
{
final public const TYPE_NONE = 'none';
final public const TYPE_BASIC = 'basic';
final public const TYPE_SELF = 'self';
final public const TYPE_ATTCA = 'attca';
/**
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
* @infection-ignore-all
*/
final public const TYPE_ECDAA = 'ecdaa';
final public const TYPE_ANONCA = 'anonca';
/**
* @param array<string, mixed> $attStmt
*/
public function __construct(
public readonly string $fmt,
public readonly array $attStmt,
public readonly string $type,
public readonly TrustPath $trustPath
) {
}
public static function create(string $fmt, array $attStmt, string $type, TrustPath $trustPath): self
{
return new self($fmt, $attStmt, $type, $trustPath);
}
/**
* @param array<string, mixed> $attStmt
*/
public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return self::create($fmt, $attStmt, self::TYPE_NONE, $trustPath);
}
/**
* @param array<string, mixed> $attStmt
*/
public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return self::create($fmt, $attStmt, self::TYPE_BASIC, $trustPath);
}
/**
* @param array<string, mixed> $attStmt
*/
public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return self::create($fmt, $attStmt, self::TYPE_SELF, $trustPath);
}
/**
* @param array<string, mixed> $attStmt
*/
public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return self::create($fmt, $attStmt, self::TYPE_ATTCA, $trustPath);
}
/**
* @param array<string, mixed> $attStmt
*
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
* @infection-ignore-all
*/
public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return self::create($fmt, $attStmt, self::TYPE_ECDAA, $trustPath);
}
/**
* @param array<string, mixed> $attStmt
*/
public static function createAnonymizationCA(string $fmt, array $attStmt, TrustPath $trustPath): self
{
return self::create($fmt, $attStmt, self::TYPE_ANONCA, $trustPath);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getFmt(): string
{
return $this->fmt;
}
/**
* @return mixed[]
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAttStmt(): array
{
return $this->attStmt;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->attStmt);
}
public function get(string $key): mixed
{
$this->has($key) || throw InvalidDataException::create($this->attStmt, sprintf(
'The attestation statement has no key "%s".',
$key
));
return $this->attStmt[$key];
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getTrustPath(): TrustPath
{
return $this->trustPath;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getType(): string
{
return $this->type;
}
/**
* @param mixed[] $data
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
* @infection-ignore-all
*/
public static function createFromArray(array $data): self
{
foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) {
array_key_exists($key, $data) || throw InvalidDataException::create($data, sprintf(
'The key "%s" is missing',
$key
));
}
return self::create(
$data['fmt'],
$data['attStmt'],
$data['type'],
TrustPathLoader::loadTrustPath($data['trustPath'])
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
trigger_deprecation(
'web-auth/webauthn-bundle',
'4.9.0',
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
__METHOD__
);
return [
'fmt' => $this->fmt,
'attStmt' => $this->attStmt,
'trustPath' => $this->trustPath,
'type' => $this->type,
];
}
}
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use Webauthn\AuthenticatorData;
interface AttestationStatementSupport
{
public function name(): string;
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement;
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool;
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use Webauthn\Exception\InvalidDataException;
use function array_key_exists;
class AttestationStatementSupportManager
{
/**
* @param AttestationStatementSupport[] $attestationStatementSupports
*/
public function __construct(
private array $attestationStatementSupports = []
) {
$this->add(new NoneAttestationStatementSupport());
foreach ($attestationStatementSupports as $attestationStatementSupport) {
$this->add($attestationStatementSupport);
}
}
/**
* @param AttestationStatementSupport[] $attestationStatementSupports
*/
public static function create(array $attestationStatementSupports = []): self
{
return new self($attestationStatementSupports);
}
public function add(AttestationStatementSupport $attestationStatementSupport): void
{
$this->attestationStatementSupports[$attestationStatementSupport->name()] = $attestationStatementSupport;
}
public function has(string $name): bool
{
return array_key_exists($name, $this->attestationStatementSupports);
}
public function get(string $name): AttestationStatementSupport
{
$this->has($name) || throw InvalidDataException::create($name, sprintf(
'The attestation statement format "%s" is not supported.',
$name
));
return $this->attestationStatementSupports[$name];
}
}
@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use CBOR\Decoder;
use CBOR\MapObject;
use Cose\Key\Ec2Key;
use Psr\EventDispatcher\EventDispatcherInterface;
use Throwable;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\Exception\AttestationStatementVerificationException;
use Webauthn\Exception\InvalidAttestationStatementException;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use function array_key_exists;
use function count;
use function is_array;
use function openssl_pkey_get_public;
use function openssl_verify;
use const OPENSSL_ALGO_SHA256;
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private readonly Decoder $decoder;
private EventDispatcherInterface $dispatcher;
public function __construct()
{
$this->decoder = Decoder::create();
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'fido-u2f';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
foreach (['sig', 'x5c'] as $key) {
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
sprintf('The attestation statement value "%s" is missing.', $key)
);
}
$certificates = $attestation['attStmt']['x5c'];
is_array($certificates) || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "x5c" must be a list with one certificate.'
);
count($certificates) === 1 || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "x5c" must be a list with one certificate.'
);
reset($certificates);
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
$this->checkCertificate($certificates[0]);
$attestationStatement = AttestationStatement::createBasic(
$attestation['fmt'],
$attestation['attStmt'],
CertificateTrustPath::create($certificates)
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$authenticatorData->attestedCredentialData
?->aaguid
->__toString() === '00000000-0000-0000-0000-000000000000' || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"'
);
$trustPath = $attestationStatement->trustPath;
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid trust path'
);
$dataToVerify = "\0";
$dataToVerify .= $authenticatorData->rpIdHash;
$dataToVerify .= $clientDataJSONHash;
$dataToVerify .= $authenticatorData->attestedCredentialData
->credentialId;
$dataToVerify .= $this->extractPublicKey($authenticatorData->attestedCredentialData ->credentialPublicKey);
return openssl_verify(
$dataToVerify,
$attestationStatement->get('sig'),
$trustPath->certificates[0],
OPENSSL_ALGO_SHA256
) === 1;
}
private function extractPublicKey(?string $publicKey): string
{
$publicKey !== null || throw AttestationStatementVerificationException::create(
'The attested credential data does not contain a valid public key.'
);
$publicKeyStream = new StringStream($publicKey);
$coseKey = $this->decoder->decode($publicKeyStream);
$publicKeyStream->isEOF() || throw AttestationStatementVerificationException::create(
'Invalid public key. Presence of extra bytes.'
);
$publicKeyStream->close();
$coseKey instanceof MapObject || throw AttestationStatementVerificationException::create(
'The attested credential data does not contain a valid public key.'
);
$coseKey = $coseKey->normalize();
$ec2Key = new Ec2Key($coseKey + [
Ec2Key::TYPE => 2,
Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256,
]);
return "\x04" . $ec2Key->x() . $ec2Key->y();
}
private function checkCertificate(string $publicKey): void
{
try {
$resource = openssl_pkey_get_public($publicKey);
$details = openssl_pkey_get_details($resource);
} catch (Throwable $throwable) {
throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain',
$throwable
);
}
is_array($details) || throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain'
);
array_key_exists('ec', $details) || throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain'
);
array_key_exists('curve_name', $details['ec']) || throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain'
);
$details['ec']['curve_name'] === 'prime256v1' || throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain'
);
array_key_exists('curve_oid', $details['ec']) || throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain'
);
$details['ec']['curve_oid'] === '1.2.840.10045.3.1.7' || throw AttestationStatementVerificationException::create(
'Invalid certificate or certificate chain'
);
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\TrustPath\EmptyTrustPath;
use function count;
use function is_array;
use function is_string;
final class NoneAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private EventDispatcherInterface $dispatcher;
public function __construct()
{
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(): self
{
return new self();
}
public function name(): string
{
return 'none';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
$format = $attestation['fmt'] ?? null;
$attestationStatement = $attestation['attStmt'] ?? [];
(is_string($format) && $format !== '') || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
(is_array(
$attestationStatement
) && $attestationStatement === []) || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
$attestationStatement = AttestationStatement::createNone(
$format,
$attestationStatement,
EmptyTrustPath::create()
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
return count($attestationStatement->attStmt) === 0;
}
}
@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use CBOR\Decoder;
use CBOR\MapObject;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\Signature;
use Cose\Algorithms;
use Cose\Key\Key;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\Exception\AttestationStatementVerificationException;
use Webauthn\Exception\InvalidAttestationStatementException;
use Webauthn\Exception\InvalidDataException;
use Webauthn\Exception\UnsupportedFeatureException;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
use Webauthn\Util\CoseSignatureFixer;
use function array_key_exists;
use function count;
use function in_array;
use function is_array;
use function is_string;
use function openssl_verify;
final class PackedAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private readonly Decoder $decoder;
private EventDispatcherInterface $dispatcher;
public function __construct(
private readonly Manager $algorithmManager
) {
$this->decoder = Decoder::create();
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(Manager $algorithmManager): self
{
return new self($algorithmManager);
}
public function name(): string
{
return 'packed';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
array_key_exists('sig', $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "sig" is missing.'
);
array_key_exists('alg', $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "alg" is missing.'
);
is_string($attestation['attStmt']['sig']) || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "sig" is missing.'
);
return match (true) {
array_key_exists('x5c', $attestation['attStmt']) => $this->loadBasicType($attestation),
array_key_exists('ecdaaKeyId', $attestation['attStmt']) => $this->loadEcdaaType($attestation['attStmt']),
default => $this->loadEmptyType($attestation),
};
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$trustPath = $attestationStatement->trustPath;
return match (true) {
$trustPath instanceof CertificateTrustPath => $this->processWithCertificate(
$clientDataJSONHash,
$attestationStatement,
$authenticatorData,
$trustPath
),
$trustPath instanceof EcdaaKeyIdTrustPath => $this->processWithECDAA(),
$trustPath instanceof EmptyTrustPath => $this->processWithSelfAttestation(
$clientDataJSONHash,
$attestationStatement,
$authenticatorData
),
default => throw InvalidAttestationStatementException::create(
$attestationStatement,
'Unsupported attestation statement'
),
};
}
/**
* @param mixed[] $attestation
*/
private function loadBasicType(array $attestation): AttestationStatement
{
$certificates = $attestation['attStmt']['x5c'];
is_array($certificates) || throw AttestationStatementVerificationException::create(
'The attestation statement value "x5c" must be a list with at least one certificate.'
);
count($certificates) > 0 || throw AttestationStatementVerificationException::create(
'The attestation statement value "x5c" must be a list with at least one certificate.'
);
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
$attestationStatement = AttestationStatement::createBasic(
$attestation['fmt'],
$attestation['attStmt'],
CertificateTrustPath::create($certificates)
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
/**
* @param array<string, mixed> $attestation
*/
private function loadEcdaaType(array $attestation): AttestationStatement
{
$ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId'];
is_string($ecdaaKeyId) || throw AttestationStatementVerificationException::create(
'The attestation statement value "ecdaaKeyId" is invalid.'
);
$attestationStatement = AttestationStatement::createEcdaa(
$attestation['fmt'],
$attestation['attStmt'],
new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId'])
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
/**
* @param mixed[] $attestation
*/
private function loadEmptyType(array $attestation): AttestationStatement
{
$attestationStatement = AttestationStatement::createSelf(
$attestation['fmt'],
$attestation['attStmt'],
EmptyTrustPath::create()
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate');
//Check version
isset($parsed['version']) || throw AttestationStatementVerificationException::create(
'Invalid certificate version'
);
$parsed['version'] === 2 || throw AttestationStatementVerificationException::create(
'Invalid certificate version'
);
//Check subject field
isset($parsed['name']) || throw AttestationStatementVerificationException::create(
'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"'
);
str_contains(
(string) $parsed['name'],
'/OU=Authenticator Attestation'
) || throw AttestationStatementVerificationException::create(
'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"'
);
//Check extensions
isset($parsed['extensions']) || throw AttestationStatementVerificationException::create(
'Certificate extensions are missing'
);
is_array($parsed['extensions']) || throw AttestationStatementVerificationException::create(
'Certificate extensions are missing'
);
//Check certificate is not a CA cert
isset($parsed['extensions']['basicConstraints']) || throw AttestationStatementVerificationException::create(
'The Basic Constraints extension must have the CA component set to false'
);
$parsed['extensions']['basicConstraints'] === 'CA:FALSE' || throw AttestationStatementVerificationException::create(
'The Basic Constraints extension must have the CA component set to false'
);
$attestedCredentialData = $authenticatorData->attestedCredentialData;
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
'No attested credential available'
);
// id-fido-gen-ce-aaguid OID check
if (in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true)) {
hash_equals(
$attestedCredentialData->aaguid
->toBinary(),
$parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']
) || throw AttestationStatementVerificationException::create(
'The value of the "aaguid" does not match with the certificate'
);
}
}
private function processWithCertificate(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData,
CertificateTrustPath $trustPath
): bool {
$certificates = $trustPath->certificates;
// Check leaf certificate
$this->checkCertificate($certificates[0], $authenticatorData);
// Get the COSE algorithm identifier and the corresponding OpenSSL one
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
// Verification of the signature
$signedData = $authenticatorData->authData . $clientDataJSONHash;
$result = openssl_verify(
$signedData,
$attestationStatement->get('sig'),
$certificates[0],
$opensslAlgorithmIdentifier
);
return $result === 1;
}
private function processWithECDAA(): never
{
throw UnsupportedFeatureException::create('ECDAA not supported');
}
private function processWithSelfAttestation(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$attestedCredentialData = $authenticatorData->attestedCredentialData;
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
'No attested credential available'
);
$credentialPublicKey = $attestedCredentialData->credentialPublicKey;
$credentialPublicKey !== null || throw AttestationStatementVerificationException::create(
'No credential public key available'
);
$publicKeyStream = new StringStream($credentialPublicKey);
$publicKey = $this->decoder->decode($publicKeyStream);
$publicKeyStream->isEOF() || throw AttestationStatementVerificationException::create(
'Invalid public key. Presence of extra bytes.'
);
$publicKeyStream->close();
$publicKey instanceof MapObject || throw AttestationStatementVerificationException::create(
'The attested credential data does not contain a valid public key.'
);
$publicKey = $publicKey->normalize();
$publicKey = new Key($publicKey);
$publicKey->alg() === (int) $attestationStatement->get(
'alg'
) || throw AttestationStatementVerificationException::create(
'The algorithm of the attestation statement and the key are not identical.'
);
$dataToVerify = $authenticatorData->authData . $clientDataJSONHash;
$algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg'));
if (! $algorithm instanceof Signature) {
throw InvalidDataException::create($algorithm, 'Invalid algorithm');
}
$signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm);
return $algorithm->verify($dataToVerify, $publicKey, $signature);
}
}
@@ -0,0 +1,445 @@
<?php
declare(strict_types=1);
namespace Webauthn\AttestationStatement;
use CBOR\Decoder;
use CBOR\MapObject;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\OkpKey;
use Cose\Key\RsaKey;
use DateTimeImmutable;
use DateTimeZone;
use Lcobucci\Clock\Clock;
use Lcobucci\Clock\SystemClock;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Psr\Clock\ClockInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Webauthn\AuthenticatorData;
use Webauthn\Event\AttestationStatementLoaded;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AttestationStatementLoadingException;
use Webauthn\Exception\AttestationStatementVerificationException;
use Webauthn\Exception\InvalidAttestationStatementException;
use Webauthn\Exception\UnsupportedFeatureException;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
use function array_key_exists;
use function count;
use function in_array;
use function is_array;
use function is_int;
use function openssl_verify;
use function unpack;
final class TPMAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
private readonly Clock|ClockInterface $clock;
private EventDispatcherInterface $dispatcher;
public function __construct(null|Clock|ClockInterface $clock = null)
{
if ($clock === null) {
trigger_deprecation(
'web-auth/metadata-service',
'4.5.0',
'The parameter "$clock" will become mandatory in 5.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new SystemClock(new DateTimeZone('UTC'));
}
$this->clock = $clock;
$this->dispatcher = new NullEventDispatcher();
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->dispatcher = $eventDispatcher;
}
public static function create(null|Clock|ClockInterface $clock = null): self
{
return new self($clock);
}
public function name(): string
{
return 'tpm';
}
/**
* @param array<string, mixed> $attestation
*/
public function load(array $attestation): AttestationStatement
{
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
! array_key_exists(
'ecdaaKeyId',
$attestation['attStmt']
) || throw AttestationStatementLoadingException::create($attestation, 'ECDAA not supported');
foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create(
$attestation,
sprintf('The attestation statement value "%s" is missing.', $key)
);
}
$attestation['attStmt']['ver'] === '2.0' || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
$certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
bin2hex((string) $certInfo['type']) === '8017' || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attestation object'
);
$pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
$pubAreaAttestedNameAlg = mb_substr((string) $certInfo['attestedName'], 0, 2, '8bit');
$pubAreaHash = hash(
$this->getTPMHash($pubAreaAttestedNameAlg),
(string) $attestation['attStmt']['pubArea'],
true
);
$attestedName = $pubAreaAttestedNameAlg . $pubAreaHash;
$attestedName === $certInfo['attestedName'] || throw AttestationStatementLoadingException::create(
$attestation,
'Invalid attested name'
);
$attestation['attStmt']['parsedCertInfo'] = $certInfo;
$attestation['attStmt']['parsedPubArea'] = $pubArea;
$certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
count($certificates) > 0 || throw AttestationStatementLoadingException::create(
$attestation,
'The attestation statement value "x5c" must be a list with at least one certificate.'
);
$attestationStatement = AttestationStatement::createAttCA(
$this->name(),
$attestation['attStmt'],
CertificateTrustPath::create($certificates)
);
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
return $attestationStatement;
}
public function isValid(
string $clientDataJSONHash,
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$attToBeSigned = $authenticatorData->authData . $clientDataJSONHash;
$attToBeSignedHash = hash(
Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')),
$attToBeSigned,
true
);
$attestationStatement->get(
'parsedCertInfo'
)['extraData'] === $attToBeSignedHash || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Invalid attestation hash'
);
$credentialPublicKey = $authenticatorData->attestedCredentialData?->credentialPublicKey;
$credentialPublicKey !== null || throw InvalidAttestationStatementException::create(
$attestationStatement,
'Not credential public key available in the attested credential data'
);
$this->checkUniquePublicKey($attestationStatement->get('parsedPubArea')['unique'], $credentialPublicKey);
return match (true) {
$attestationStatement->trustPath instanceof CertificateTrustPath => $this->processWithCertificate(
$attestationStatement,
$authenticatorData
),
$attestationStatement->trustPath instanceof EcdaaKeyIdTrustPath => $this->processWithECDAA(),
default => throw InvalidAttestationStatementException::create(
$attestationStatement,
'Unsupported attestation statement'
),
};
}
private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
{
$cborDecoder = Decoder::create();
$publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
$publicKey instanceof MapObject || throw AttestationStatementVerificationException::create(
'Invalid public key'
);
$key = Key::create($publicKey->normalize());
switch ($key->type()) {
case Key::TYPE_OKP:
$uniqueFromKey = (new OkpKey($key->getData()))->x();
break;
case Key::TYPE_EC2:
$ec2Key = new Ec2Key($key->getData());
$uniqueFromKey = "\x04" . $ec2Key->x() . $ec2Key->y();
break;
case Key::TYPE_RSA:
$uniqueFromKey = (new RsaKey($key->getData()))->n();
break;
default:
throw AttestationStatementVerificationException::create('Invalid or unsupported key type.');
}
$unique === $uniqueFromKey || throw AttestationStatementVerificationException::create(
'Invalid pubArea.unique value'
);
}
/**
* @return mixed[]
*/
private function checkCertInfo(string $data): array
{
$certInfo = new StringStream($data);
$magic = $certInfo->read(4);
bin2hex($magic) === 'ff544347' || throw AttestationStatementVerificationException::create(
'Invalid attestation object'
);
$type = $certInfo->read(2);
$qualifiedSignerLength = unpack('n', $certInfo->read(2))[1];
$qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored
$extraDataLength = unpack('n', $certInfo->read(2))[1];
$extraData = $certInfo->read($extraDataLength);
$clockInfo = $certInfo->read(17); //Ignore
$firmwareVersion = $certInfo->read(8);
$attestedNameLength = unpack('n', $certInfo->read(2))[1];
$attestedName = $certInfo->read($attestedNameLength);
$attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1];
$attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore
$certInfo->isEOF() || throw AttestationStatementVerificationException::create(
'Invalid certificate information. Presence of extra bytes.'
);
$certInfo->close();
return [
'magic' => $magic,
'type' => $type,
'qualifiedSigner' => $qualifiedSigner,
'extraData' => $extraData,
'clockInfo' => $clockInfo,
'firmwareVersion' => $firmwareVersion,
'attestedName' => $attestedName,
'attestedQualifiedName' => $attestedQualifiedName,
];
}
/**
* @return mixed[]
*/
private function checkPubArea(string $data): array
{
$pubArea = new StringStream($data);
$type = $pubArea->read(2);
$nameAlg = $pubArea->read(2);
$objectAttributes = $pubArea->read(4);
$authPolicyLength = unpack('n', $pubArea->read(2))[1];
$authPolicy = $pubArea->read($authPolicyLength);
$parameters = $this->getParameters($type, $pubArea);
$unique = $this->getUnique($type, $pubArea);
$pubArea->isEOF() || throw AttestationStatementVerificationException::create(
'Invalid public area. Presence of extra bytes.'
);
$pubArea->close();
return [
'type' => $type,
'nameAlg' => $nameAlg,
'objectAttributes' => $objectAttributes,
'authPolicy' => $authPolicy,
'parameters' => $parameters,
'unique' => $unique,
];
}
/**
* @return mixed[]
*/
private function getParameters(string $type, StringStream $stream): array
{
return match (bin2hex($type)) {
'0001' => [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'keyBits' => unpack('n', $stream->read(2))[1],
'exponent' => $this->getExponent($stream->read(4)),
],
'0023' => [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'curveId' => $stream->read(2),
'kdf' => $stream->read(2),
],
default => throw AttestationStatementVerificationException::create('Unsupported type'),
};
}
private function getUnique(string $type, StringStream $stream): string
{
switch (bin2hex($type)) {
case '0001':
$uniqueLength = unpack('n', $stream->read(2))[1];
return $stream->read($uniqueLength);
case '0023':
$xLen = unpack('n', $stream->read(2))[1];
$x = $stream->read($xLen);
$yLen = unpack('n', $stream->read(2))[1];
$y = $stream->read($yLen);
return "\04" . $x . $y;
default:
throw AttestationStatementVerificationException::create('Unsupported type');
}
}
private function getExponent(string $exponent): string
{
return bin2hex($exponent) === '00000000' ? Base64UrlSafe::decodeNoPadding('AQAB') : $exponent;
}
private function getTPMHash(string $nameAlg): string
{
return match (bin2hex($nameAlg)) {
'0004' => 'sha1',
'000b' => 'sha256',
'000c' => 'sha384',
'000d' => 'sha512',
default => throw AttestationStatementVerificationException::create('Unsupported hash algorithm'),
};
}
private function processWithCertificate(
AttestationStatement $attestationStatement,
AuthenticatorData $authenticatorData
): bool {
$trustPath = $attestationStatement->trustPath;
$trustPath instanceof CertificateTrustPath || throw AttestationStatementVerificationException::create(
'Invalid trust path'
);
$certificates = $trustPath->certificates;
// Check certificate CA chain and returns the Attestation Certificate
$this->checkCertificate($certificates[0], $authenticatorData);
// Get the COSE algorithm identifier and the corresponding OpenSSL one
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
$result = openssl_verify(
$attestationStatement->get('certInfo'),
$attestationStatement->get('sig'),
$certificates[0],
$opensslAlgorithmIdentifier
);
return $result === 1;
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate');
//Check version
(isset($parsed['version']) && $parsed['version'] === 2) || throw AttestationStatementVerificationException::create(
'Invalid certificate version'
);
//Check subject field is empty
isset($parsed['subject']) || throw AttestationStatementVerificationException::create(
'Invalid certificate name. The Subject should be empty'
);
is_array($parsed['subject']) || throw AttestationStatementVerificationException::create(
'Invalid certificate name. The Subject should be empty'
);
count($parsed['subject']) === 0 || throw AttestationStatementVerificationException::create(
'Invalid certificate name. The Subject should be empty'
);
// Check period of validity
array_key_exists(
'validFrom_time_t',
$parsed
) || throw AttestationStatementVerificationException::create('Invalid certificate start date.');
is_int($parsed['validFrom_time_t']) || throw AttestationStatementVerificationException::create(
'Invalid certificate start date.'
);
$startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
$startDate < $this->clock->now() || throw AttestationStatementVerificationException::create(
'Invalid certificate start date.'
);
array_key_exists('validTo_time_t', $parsed) || throw AttestationStatementVerificationException::create(
'Invalid certificate end date.'
);
is_int($parsed['validTo_time_t']) || throw AttestationStatementVerificationException::create(
'Invalid certificate end date.'
);
$endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
$endDate > $this->clock->now() || throw AttestationStatementVerificationException::create(
'Invalid certificate end date.'
);
//Check extensions
(isset($parsed['extensions']) && is_array(
$parsed['extensions']
)) || throw AttestationStatementVerificationException::create('Certificate extensions are missing');
//Check subjectAltName
isset($parsed['extensions']['subjectAltName']) || throw AttestationStatementVerificationException::create(
'The "subjectAltName" is missing'
);
//Check extendedKeyUsage
isset($parsed['extensions']['extendedKeyUsage']) || throw AttestationStatementVerificationException::create(
'The "subjectAltName" is missing'
);
$parsed['extensions']['extendedKeyUsage'] === '2.23.133.8.3' || throw AttestationStatementVerificationException::create(
'The "extendedKeyUsage" is invalid'
);
// id-fido-gen-ce-aaguid OID check
in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && ! hash_equals(
$authenticatorData->attestedCredentialData
?->aaguid
->toBinary() ?? '',
$parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']
) && throw AttestationStatementVerificationException::create(
'The value of the "aaguid" does not match with the certificate'
);
}
private function processWithECDAA(): never
{
throw UnsupportedFeatureException::create('ECDAA not supported');
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use JsonSerializable;
use ParagonIE\ConstantTime\Base64;
use Symfony\Component\Uid\Uuid;
use Webauthn\Exception\InvalidDataException;
use function array_key_exists;
use function is_string;
/**
* @see https://www.w3.org/TR/webauthn/#sec-attested-credential-data
*/
class AttestedCredentialData implements JsonSerializable
{
public function __construct(
public Uuid $aaguid,
public readonly string $credentialId,
public readonly ?string $credentialPublicKey
) {
}
public static function create(Uuid $aaguid, string $credentialId, ?string $credentialPublicKey = null): self
{
return new self($aaguid, $credentialId, $credentialPublicKey);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAaguid(): Uuid
{
return $this->aaguid;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function setAaguid(Uuid $aaguid): void
{
$this->aaguid = $aaguid;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getCredentialId(): string
{
return $this->credentialId;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getCredentialPublicKey(): ?string
{
return $this->credentialPublicKey;
}
/**
* @param mixed[] $json
* @deprecated since 4.9.0 and will be removed in 5.0.0. Please use the serializer instead.
*/
public static function createFromArray(array $json): self
{
array_key_exists('aaguid', $json) || throw InvalidDataException::create(
$json,
'Invalid input. "aaguid" is missing.'
);
$aaguid = $json['aaguid'];
is_string($aaguid) || throw InvalidDataException::create(
$json,
'Invalid input. "aaguid" shall be a string of 36 characters'
);
mb_strlen($aaguid, '8bit') === 36 || throw InvalidDataException::create(
$json,
'Invalid input. "aaguid" shall be a string of 36 characters'
);
$uuid = Uuid::fromString($aaguid);
array_key_exists('credentialId', $json) || throw InvalidDataException::create(
$json,
'Invalid input. "credentialId" is missing.'
);
$credentialId = $json['credentialId'];
is_string($credentialId) || throw InvalidDataException::create(
$json,
'Invalid input. "credentialId" shall be a string'
);
$credentialId = Base64::decode($credentialId, true);
$credentialPublicKey = null;
if (isset($json['credentialPublicKey'])) {
$credentialPublicKey = Base64::decode($json['credentialPublicKey'], true);
}
return self::create($uuid, $credentialId, $credentialPublicKey);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
trigger_deprecation(
'web-auth/webauthn-bundle',
'4.9.0',
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
__METHOD__
);
$result = [
'aaguid' => $this->aaguid->__toString(),
'credentialId' => base64_encode($this->credentialId),
];
if ($this->credentialPublicKey !== null) {
$result['credentialPublicKey'] = base64_encode($this->credentialPublicKey);
}
return $result;
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
use JsonSerializable;
class AuthenticationExtension implements JsonSerializable
{
public function __construct(
public readonly string $name,
public readonly mixed $value
) {
}
public static function create(string $name, mixed $value): self
{
return new self($name, $value);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function name(): string
{
return $this->name;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function value(): mixed
{
return $this->value;
}
public function jsonSerialize(): mixed
{
trigger_deprecation(
'web-auth/webauthn-bundle',
'4.9.0',
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
__METHOD__
);
return $this->value;
}
}
@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
use ArrayAccess;
use ArrayIterator;
use Countable;
use Iterator;
use IteratorAggregate;
use JsonSerializable;
use Webauthn\Exception\AuthenticationExtensionException;
use function array_key_exists;
use function count;
use function is_string;
use const COUNT_NORMAL;
/**
* @implements IteratorAggregate<AuthenticationExtension>
* @final
*/
class AuthenticationExtensions implements JsonSerializable, Countable, IteratorAggregate, ArrayAccess
{
/**
* @var array<string, AuthenticationExtension>
* @readonly
*/
public array $extensions;
/**
* @param array<array-key, mixed|AuthenticationExtension> $extensions
*/
public function __construct(array $extensions = [])
{
$list = [];
foreach ($extensions as $key => $extension) {
if ($extension instanceof AuthenticationExtension) {
$list[$extension->name] = $extension;
continue;
}
if (is_string($key)) {
$list[$key] = AuthenticationExtension::create($key, $extension);
continue;
}
throw new AuthenticationExtensionException('Invalid extension');
}
$this->extensions = $list;
}
/**
* @param array<array-key, AuthenticationExtension> $extensions
*/
public static function create(array $extensions = []): static
{
return new static($extensions);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function add(AuthenticationExtension ...$extensions): static
{
foreach ($extensions as $extension) {
$this->extensions[$extension->name] = $extension;
}
return $this;
}
/**
* @param array<string, mixed> $json
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
* @infection-ignore-all
*/
public static function createFromArray(array $json): static
{
return static::create(
array_map(
static fn (string $key, mixed $value): AuthenticationExtension => AuthenticationExtension::create(
$key,
$value
),
array_keys($json),
$json
)
);
}
public function has(string $key): bool
{
return array_key_exists($key, $this->extensions);
}
public function get(string $key): AuthenticationExtension
{
$this->has($key) || throw AuthenticationExtensionException::create(sprintf(
'The extension with key "%s" is not available',
$key
));
return $this->extensions[$key];
}
/**
* @return array<string, AuthenticationExtension>
*/
public function jsonSerialize(): array
{
trigger_deprecation(
'web-auth/webauthn-bundle',
'4.9.0',
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
__METHOD__
);
return $this->extensions;
}
/**
* @return Iterator<string, AuthenticationExtension>
*/
public function getIterator(): Iterator
{
return new ArrayIterator($this->extensions);
}
public function count(int $mode = COUNT_NORMAL): int
{
return count($this->extensions, $mode);
}
public function offsetExists(mixed $offset): bool
{
return array_key_exists($offset, $this->extensions);
}
public function offsetGet(mixed $offset): mixed
{
return $this->extensions[$offset];
}
public function offsetSet(mixed $offset, mixed $value): void
{
if ($value === null) {
return;
}
if ($value instanceof AuthenticationExtension) {
$this->extensions[$value->name] = $value;
return;
}
if (is_string($offset)) {
$this->extensions[$offset] = AuthenticationExtension::create($offset, $value);
return;
}
throw new AuthenticationExtensionException('Invalid extension');
}
public function offsetUnset(mixed $offset): void
{
unset($this->extensions[$offset]);
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
/**
* @deprecated since 4.8.0. Use {Webauthn\AuthenticationExtensions\AuthenticationExtensions} instead.
*/
class AuthenticationExtensionsClientInputs extends AuthenticationExtensions
{
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
/**
* @deprecated since 4.8.0. Use {Webauthn\AuthenticationExtensions\AuthenticationExtensions} instead.
*/
class AuthenticationExtensionsClientOutputs extends AuthenticationExtensions
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
use CBOR\CBORObject;
use CBOR\MapObject;
use Webauthn\Exception\AuthenticationExtensionException;
abstract class AuthenticationExtensionsClientOutputsLoader
{
public static function load(CBORObject $object): AuthenticationExtensions
{
$object instanceof MapObject || throw AuthenticationExtensionException::create('Invalid extension object');
$data = $object->normalize();
return AuthenticationExtensionsClientOutputs::create(
array_map(
fn (mixed $value, string $key) => AuthenticationExtension::create($key, $value),
$data,
array_keys($data)
)
);
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
interface ExtensionOutputChecker
{
public function check(AuthenticationExtensions $inputs, AuthenticationExtensions $outputs): void;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
class ExtensionOutputCheckerHandler
{
/**
* @var ExtensionOutputChecker[]
*/
private array $checkers = [];
public static function create(): self
{
return new self();
}
public function add(ExtensionOutputChecker $checker): void
{
$this->checkers[] = $checker;
}
public function check(AuthenticationExtensions $inputs, AuthenticationExtensions $outputs): void
{
foreach ($this->checkers as $checker) {
$checker->check($inputs, $outputs);
}
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Webauthn\AuthenticationExtensions;
use Exception;
use Throwable;
class ExtensionOutputError extends Exception
{
public function __construct(
public readonly AuthenticationExtension $authenticationExtension,
string $message = '',
int $code = 0,
Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAuthenticationExtension(): AuthenticationExtension
{
return $this->authenticationExtension;
}
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use Webauthn\AttestationStatement\AttestationObject;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
*/
class AuthenticatorAssertionResponse extends AuthenticatorResponse
{
public function __construct(
CollectedClientData $clientDataJSON,
public readonly AuthenticatorData $authenticatorData,
public readonly string $signature,
public readonly ?string $userHandle,
public readonly null|AttestationObject $attestationObject = null,
) {
parent::__construct($clientDataJSON);
}
public static function create(
CollectedClientData $clientDataJSON,
AuthenticatorData $authenticatorData,
string $signature,
?string $userHandle = null,
null|AttestationObject $attestationObject = null,
): self {
return new self($clientDataJSON, $authenticatorData, $signature, $userHandle, $attestationObject);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getSignature(): string
{
return $this->signature;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getUserHandle(): ?string
{
return $this->userHandle;
}
}
@@ -0,0 +1,325 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use Cose\Algorithm\Manager;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Throwable;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\CeremonyStep\CeremonyStepManager;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\Counter\CounterChecker;
use Webauthn\Event\AuthenticatorAssertionResponseValidationFailedEvent;
use Webauthn\Event\AuthenticatorAssertionResponseValidationSucceededEvent;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\MetadataService\CanLogData;
use Webauthn\TokenBinding\TokenBindingHandler;
use function is_string;
class AuthenticatorAssertionResponseValidator implements CanLogData, CanDispatchEvents
{
private LoggerInterface $logger;
private readonly CeremonyStepManagerFactory $ceremonyStepManagerFactory;
private EventDispatcherInterface $eventDispatcher;
public function __construct(
private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
private readonly null|TokenBindingHandler $tokenBindingHandler = null,
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
null|Manager $algorithmManager = null,
null|EventDispatcherInterface $eventDispatcher = null,
private null|CeremonyStepManager $ceremonyStepManager = null
) {
if ($this->publicKeyCredentialSourceRepository !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.6.0',
'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.'
);
}
if ($this->tokenBindingHandler !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.3.0',
'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.'
);
}
if ($extensionOutputCheckerHandler !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.'
);
}
if ($algorithmManager !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$algorithmManager" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckSignature object into the CeremonyStepManager.'
);
}
$this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher();
if ($eventDispatcher !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.'
);
}
if ($this->ceremonyStepManager === null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $algorithmManager and $extensionOutputCheckerHandler.'
);
}
$this->logger = new NullLogger();
$this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
if ($extensionOutputCheckerHandler !== null) {
$this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler);
}
if ($algorithmManager !== null) {
$this->ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager);
}
}
public static function create(
null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
null|TokenBindingHandler $tokenBindingHandler = null,
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
null|Manager $algorithmManager = null,
null|EventDispatcherInterface $eventDispatcher = null,
null|CeremonyStepManager $ceremonyStepManager = null
): self {
return new self(
$publicKeyCredentialSourceRepository,
$tokenBindingHandler,
$extensionOutputCheckerHandler,
$algorithmManager,
$eventDispatcher,
$ceremonyStepManager
);
}
/**
* @param string[] $securedRelyingPartyId
*
* @see https://www.w3.org/TR/webauthn/#verifying-assertion
*/
public function check(
string|PublicKeyCredentialSource $credentialId,
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
ServerRequestInterface|string $request,
?string $userHandle,
null|array $securedRelyingPartyId = null
): PublicKeyCredentialSource {
if ($request instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to the method `check` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
ServerRequestInterface::class,
self::class
)
);
}
if (is_string($credentialId)) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.6.0',
sprintf(
'Passing a string as first to the method `check` of the class "%s" is deprecated since 4.6.0. Please inject a %s object instead.',
self::class,
PublicKeyCredentialSource::class
)
);
}
if ($securedRelyingPartyId !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
sprintf(
'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckOrigin into the CeremonyStepManager instead.',
self::class
)
);
}
if ($credentialId instanceof PublicKeyCredentialSource) {
$publicKeyCredentialSource = $credentialId;
} else {
$this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository || throw AuthenticatorResponseVerificationException::create(
'Please pass the Public Key Credential Source to the method "check".'
);
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId(
$credentialId
);
}
$publicKeyCredentialSource !== null || throw AuthenticatorResponseVerificationException::create(
'The credential ID is invalid.'
);
$host = is_string($request) ? $request : $request->getUri()
->getHost();
if ($this->ceremonyStepManager === null) {
$this->ceremonyStepManager = $this->ceremonyStepManagerFactory->requestCeremony($securedRelyingPartyId);
}
try {
$this->logger->info('Checking the authenticator assertion response', [
'credentialId' => $credentialId,
'publicKeyCredentialSource' => $publicKeyCredentialSource,
'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
'host' => $host,
'userHandle' => $userHandle,
]);
$this->ceremonyStepManager->process(
$publicKeyCredentialSource,
$authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions,
$userHandle,
$host
);
$publicKeyCredentialSource->counter = $authenticatorAssertionResponse->authenticatorData->signCount; //26.1.
$publicKeyCredentialSource->backupEligible = $authenticatorAssertionResponse->authenticatorData->isBackupEligible(); //26.2.
$publicKeyCredentialSource->backupStatus = $authenticatorAssertionResponse->authenticatorData->isBackedUp(); //26.2.
if ($publicKeyCredentialSource->uvInitialized === false) {
$publicKeyCredentialSource->uvInitialized = $authenticatorAssertionResponse->authenticatorData->isUserVerified(); //26.3.
}
/*
* 26.3.
* OPTIONALLY, if response.attestationObject is present, update credentialRecord.attestationObject to the value of response.attestationObject and update credentialRecord.attestationClientDataJSON to the value of response.clientDataJSON.
*/
if (is_string(
$credentialId
) && ($this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository)) {
$this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
}
//All good. We can continue.
$this->logger->info('The assertion is valid');
$this->logger->debug('Public Key Credential Source', [
'publicKeyCredentialSource' => $publicKeyCredentialSource,
]);
$this->eventDispatcher->dispatch(
$this->createAuthenticatorAssertionResponseValidationSucceededEvent(
null,
$authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions,
$host,
$userHandle,
$publicKeyCredentialSource
)
);
// 27.
return $publicKeyCredentialSource;
} catch (AuthenticatorResponseVerificationException $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
$this->eventDispatcher->dispatch(
$this->createAuthenticatorAssertionResponseValidationFailedEvent(
$publicKeyCredentialSource,
$authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions,
$host,
$userHandle,
$throwable
)
);
throw $throwable;
}
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* @deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckCounter object into a CeremonyStepManager instead.
*/
public function setCounterChecker(CounterChecker $counterChecker): self
{
$this->ceremonyStepManagerFactory->setCounterChecker($counterChecker);
return $this;
}
protected function createAuthenticatorAssertionResponseValidationSucceededEvent(
null|string $credentialId,
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
ServerRequestInterface|string $host,
?string $userHandle,
PublicKeyCredentialSource $publicKeyCredentialSource
): AuthenticatorAssertionResponseValidationSucceededEvent {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to the method `createAuthenticatorAssertionResponseValidationSucceededEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
ServerRequestInterface::class,
self::class
)
);
}
return new AuthenticatorAssertionResponseValidationSucceededEvent(
$credentialId,
$authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions,
$host,
$userHandle,
$publicKeyCredentialSource
);
}
protected function createAuthenticatorAssertionResponseValidationFailedEvent(
string|PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
ServerRequestInterface|string $host,
?string $userHandle,
Throwable $throwable
): AuthenticatorAssertionResponseValidationFailedEvent {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to the method `createAuthenticatorAssertionResponseValidationFailedEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
ServerRequestInterface::class,
self::class
)
);
}
return new AuthenticatorAssertionResponseValidationFailedEvent(
$publicKeyCredentialSource,
$authenticatorAssertionResponse,
$publicKeyCredentialRequestOptions,
$host,
$userHandle,
$throwable
);
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use Webauthn\AttestationStatement\AttestationObject;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
*/
class AuthenticatorAttestationResponse extends AuthenticatorResponse
{
/**
* @param string[] $transports
*/
public function __construct(
CollectedClientData $clientDataJSON,
public readonly AttestationObject $attestationObject,
public readonly array $transports = []
) {
parent::__construct($clientDataJSON);
}
/**
* @param string[] $transports
*/
public static function create(
CollectedClientData $clientDataJSON,
AttestationObject $attestationObject,
array $transports = []
): self {
return new self($clientDataJSON, $attestationObject, $transports);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAttestationObject(): AttestationObject
{
return $this->attestationObject;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*
* @return string[]
*/
public function getTransports(): array
{
return $this->transports;
}
}
@@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Throwable;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\CeremonyStep\CeremonyStepManager;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\Event\AuthenticatorAttestationResponseValidationFailedEvent;
use Webauthn\Event\AuthenticatorAttestationResponseValidationSucceededEvent;
use Webauthn\Event\CanDispatchEvents;
use Webauthn\Event\NullEventDispatcher;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\MetadataService\CanLogData;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\StatusReportRepository;
use Webauthn\TokenBinding\TokenBindingHandler;
use function is_string;
class AuthenticatorAttestationResponseValidator implements CanLogData, CanDispatchEvents
{
private LoggerInterface $logger;
private EventDispatcherInterface $eventDispatcher;
private readonly CeremonyStepManagerFactory $ceremonyStepManagerFactory;
public function __construct(
null|AttestationStatementSupportManager $attestationStatementSupportManager = null,
private readonly null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
private readonly null|TokenBindingHandler $tokenBindingHandler = null,
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
null|EventDispatcherInterface $eventDispatcher = null,
private null|CeremonyStepManager $ceremonyStepManager = null
) {
if ($this->publicKeyCredentialSourceRepository !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.6.0',
'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.'
);
}
if ($this->tokenBindingHandler !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.3.0',
'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.'
);
}
if ($extensionOutputCheckerHandler !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.'
);
}
$this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher();
if ($eventDispatcher !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.'
);
}
if ($this->ceremonyStepManager === null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $attestationStatementSupportManager and $extensionOutputCheckerHandler.'
);
}
$this->logger = new NullLogger();
$this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
if ($attestationStatementSupportManager !== null) {
$this->ceremonyStepManagerFactory->setAttestationStatementSupportManager(
$attestationStatementSupportManager
);
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
'The parameter "$attestationStatementSupportManager" is deprecated since 4.8.0 will be removed in 5.0.0. Please set a CheckAttestationFormatIsKnownAndValid object into CeremonyStepManager object instead.'
);
}
if ($extensionOutputCheckerHandler !== null) {
$this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler);
}
}
/**
* @private Will become private in 5.0.0
*/
public static function create(
null|AttestationStatementSupportManager $attestationStatementSupportManager = null,
null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null,
null|TokenBindingHandler $tokenBindingHandler = null,
null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null,
null|EventDispatcherInterface $eventDispatcher = null,
null|CeremonyStepManager $ceremonyStepManager = null,
): self {
return new self(
$attestationStatementSupportManager,
$publicKeyCredentialSourceRepository,
$tokenBindingHandler,
$extensionOutputCheckerHandler,
$eventDispatcher,
$ceremonyStepManager
);
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead.
*/
public function setCertificateChainValidator(CertificateChainValidator $certificateChainValidator): self
{
$this->ceremonyStepManagerFactory->enableCertificateChainValidator($certificateChainValidator);
return $this;
}
/**
* @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead.
*/
public function enableMetadataStatementSupport(
MetadataStatementRepository $metadataStatementRepository,
StatusReportRepository $statusReportRepository,
CertificateChainValidator $certificateChainValidator
): self {
$this->ceremonyStepManagerFactory->enableMetadataStatementSupport(
$metadataStatementRepository,
$statusReportRepository,
$certificateChainValidator
);
return $this;
}
/**
* @param string[] $securedRelyingPartyId
*
* @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
*/
public function check(
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
ServerRequestInterface|string $request,
null|array $securedRelyingPartyId = null,
): PublicKeyCredentialSource {
if ($request instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to the method `check` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
ServerRequestInterface::class,
self::class
)
);
}
if ($securedRelyingPartyId !== null) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.8.0',
sprintf(
'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject the list instead.',
self::class
)
);
}
$host = is_string($request) ? $request : $request->getUri()
->getHost();
try {
$this->logger->info('Checking the authenticator attestation response', [
'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
'host' => $host,
]);
if ($this->ceremonyStepManager === null) {
$this->ceremonyStepManager = $this->ceremonyStepManagerFactory->creationCeremony(
$securedRelyingPartyId
);
}
$publicKeyCredentialSource = $this->createPublicKeyCredentialSource(
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions
);
$this->ceremonyStepManager->process(
$publicKeyCredentialSource,
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions,
$publicKeyCredentialCreationOptions->user->id,
$host
);
$publicKeyCredentialSource->counter = $authenticatorAttestationResponse->attestationObject->authData->signCount;
$publicKeyCredentialSource->backupEligible = $authenticatorAttestationResponse->attestationObject->authData->isBackupEligible();
$publicKeyCredentialSource->backupStatus = $authenticatorAttestationResponse->attestationObject->authData->isBackedUp();
$publicKeyCredentialSource->uvInitialized = $authenticatorAttestationResponse->attestationObject->authData->isUserVerified();
$this->logger->info('The attestation is valid');
$this->logger->debug('Public Key Credential Source', [
'publicKeyCredentialSource' => $publicKeyCredentialSource,
]);
$this->eventDispatcher->dispatch(
$this->createAuthenticatorAttestationResponseValidationSucceededEvent(
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions,
$host,
$publicKeyCredentialSource
)
);
return $publicKeyCredentialSource;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
$this->eventDispatcher->dispatch(
$this->createAuthenticatorAttestationResponseValidationFailedEvent(
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions,
$host,
$throwable
)
);
throw $throwable;
}
}
protected function createAuthenticatorAttestationResponseValidationSucceededEvent(
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
ServerRequestInterface|string $host,
PublicKeyCredentialSource $publicKeyCredentialSource
): AuthenticatorAttestationResponseValidationSucceededEvent {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to the method `createAuthenticatorAttestationResponseValidationSucceededEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
ServerRequestInterface::class,
self::class
)
);
}
return new AuthenticatorAttestationResponseValidationSucceededEvent(
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions,
$host,
$publicKeyCredentialSource
);
}
protected function createAuthenticatorAttestationResponseValidationFailedEvent(
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
ServerRequestInterface|string $host,
Throwable $throwable
): AuthenticatorAttestationResponseValidationFailedEvent {
if ($host instanceof ServerRequestInterface) {
trigger_deprecation(
'web-auth/webauthn-lib',
'4.5.0',
sprintf(
'Passing a %s to the method `createAuthenticatorAttestationResponseValidationFailedEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.',
ServerRequestInterface::class,
self::class
)
);
}
return new AuthenticatorAttestationResponseValidationFailedEvent(
$authenticatorAttestationResponse,
$publicKeyCredentialCreationOptions,
$host,
$throwable
);
}
private function createPublicKeyCredentialSource(
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
): PublicKeyCredentialSource {
$attestationObject = $authenticatorAttestationResponse->attestationObject;
$attestedCredentialData = $attestationObject->authData->attestedCredentialData;
$attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
'Not attested credential data'
);
$credentialId = $attestedCredentialData->credentialId;
$credentialPublicKey = $attestedCredentialData->credentialPublicKey;
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
'Not credential public key available in the attested credential data'
);
$userHandle = $publicKeyCredentialCreationOptions->user->id;
$transports = $authenticatorAttestationResponse->transports;
return PublicKeyCredentialSource::create(
$credentialId,
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
$transports,
$attestationObject->attStmt
->type,
$attestationObject->attStmt
->trustPath,
$attestedCredentialData->aaguid,
$credentialPublicKey,
$userHandle,
$attestationObject->authData
->signCount,
);
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use function ord;
/**
* @see https://www.w3.org/TR/webauthn/#sec-authenticator-data
* @see https://www.w3.org/TR/webauthn/#flags
*/
class AuthenticatorData
{
final public const FLAG_UP = 0b00000001;
final public const FLAG_RFU1 = 0b00000010;
final public const FLAG_UV = 0b00000100;
final public const FLAG_BE = 0b00001000;
final public const FLAG_BS = 0b00010000;
/**
* TODO: remove bits 3 and 4 as they have been assigned to BE and BS in Webauthn level 3.
*/
final public const FLAG_RFU2 = 0b00111000;
final public const FLAG_AT = 0b01000000;
final public const FLAG_ED = 0b10000000;
public function __construct(
public readonly string $authData,
public readonly string $rpIdHash,
public readonly string $flags,
public readonly int $signCount,
public readonly null|AttestedCredentialData $attestedCredentialData,
public readonly null|AuthenticationExtensions $extensions
) {
}
public static function create(
string $authData,
string $rpIdHash,
string $flags,
int $signCount,
null|AttestedCredentialData $attestedCredentialData = null,
null|AuthenticationExtensions $extensions = null
): self {
return new self($authData, $rpIdHash, $flags, $signCount, $attestedCredentialData, $extensions);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAuthData(): string
{
return $this->authData;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getRpIdHash(): string
{
return $this->rpIdHash;
}
public function isUserPresent(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_UP);
}
public function isUserVerified(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_UV);
}
public function isBackupEligible(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_BE);
}
public function isBackedUp(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_BS);
}
public function hasAttestedCredentialData(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_AT);
}
public function hasExtensions(): bool
{
return 0 !== (ord($this->flags) & self::FLAG_ED);
}
public function getReservedForFutureUse1(): int
{
return ord($this->flags) & self::FLAG_RFU1;
}
public function getReservedForFutureUse2(): int
{
return ord($this->flags) & self::FLAG_RFU2;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getSignCount(): int
{
return $this->signCount;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAttestedCredentialData(): ?AttestedCredentialData
{
return $this->attestedCredentialData;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getExtensions(): ?AuthenticationExtensions
{
return $this->extensions !== null && $this->hasExtensions() ? $this->extensions : null;
}
}
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use CBOR\ByteStringObject;
use CBOR\Decoder;
use CBOR\ListObject;
use CBOR\MapObject;
use CBOR\NegativeIntegerObject;
use CBOR\TextStringObject;
use CBOR\UnsignedIntegerObject;
use Symfony\Component\Uid\Uuid;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
use Webauthn\Exception\InvalidDataException;
use function chr;
use function ord;
final class AuthenticatorDataLoader
{
private readonly Decoder $decoder;
private function __construct()
{
$this->decoder = Decoder::create();
}
public static function create(): self
{
return new self();
}
public function load(string $authData): AuthenticatorData
{
$authData = $this->fixIncorrectEdDSAKey($authData);
$authDataStream = new StringStream($authData);
$rp_id_hash = $authDataStream->read(32);
$flags = $authDataStream->read(1);
$signCount = $authDataStream->read(4);
$signCount = unpack('N', $signCount);
$attestedCredentialData = null;
if (0 !== (ord($flags) & AuthenticatorData::FLAG_AT)) {
$aaguid = Uuid::fromBinary($authDataStream->read(16));
$credentialLength = $authDataStream->read(2);
$credentialLength = unpack('n', $credentialLength);
$credentialId = $authDataStream->read($credentialLength[1]);
$credentialPublicKey = $this->decoder->decode($authDataStream);
$credentialPublicKey instanceof MapObject || throw InvalidDataException::create(
$authData,
'The data does not contain a valid credential public key.'
);
$attestedCredentialData = AttestedCredentialData::create(
$aaguid,
$credentialId,
(string) $credentialPublicKey
);
}
$extension = null;
if (0 !== (ord($flags) & AuthenticatorData::FLAG_ED)) {
$extension = $this->decoder->decode($authDataStream);
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
}
$authDataStream->isEOF() || throw InvalidDataException::create(
$authData,
'Invalid authentication data. Presence of extra bytes.'
);
$authDataStream->close();
return AuthenticatorData::create(
$authData,
$rp_id_hash,
$flags,
$signCount[1],
$attestedCredentialData,
$extension
);
}
private function fixIncorrectEdDSAKey(string $data): string
{
$needle = hex2bin('a301634f4b500327206745643235353139');
$correct = hex2bin('a401634f4b500327206745643235353139');
$position = mb_strpos($data, $needle, 0, '8bit');
if ($position === false) {
return $data;
}
$begin = mb_substr($data, 0, $position, '8bit');
$end = mb_substr($data, $position, null, '8bit');
$end = str_replace($needle, $correct, $end);
$cbor = new StringStream($end);
$badKey = $this->decoder->decode($cbor);
($badKey instanceof MapObject && $cbor->isEOF()) || throw InvalidDataException::create(
$end,
'Invalid authentication data. Presence of extra bytes.'
);
$badX = $badKey->get(-2);
$badX instanceof ListObject || throw InvalidDataException::create($end, 'Invalid authentication data.');
$keyBytes = array_reduce(
$badX->normalize(),
static fn (string $carry, string $item): string => $carry . chr((int) $item),
''
);
$correctX = ByteStringObject::create($keyBytes);
$correctKey = MapObject::create()
->add(UnsignedIntegerObject::create(1), TextStringObject::create('OKP'))
->add(UnsignedIntegerObject::create(3), NegativeIntegerObject::create(-8))
->add(NegativeIntegerObject::create(-1), TextStringObject::create('Ed25519'))
->add(NegativeIntegerObject::create(-2), $correctX);
return $begin . $correctKey;
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Webauthn;
/**
* @see https://www.w3.org/TR/webauthn/#authenticatorresponse
*/
abstract class AuthenticatorResponse
{
public function __construct(
public readonly CollectedClientData $clientDataJSON
) {
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getClientDataJSON(): CollectedClientData
{
return $this->clientDataJSON;
}
}
@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use InvalidArgumentException;
use JsonSerializable;
use Webauthn\Exception\InvalidDataException;
use function in_array;
use function is_bool;
use function is_string;
use const JSON_THROW_ON_ERROR;
class AuthenticatorSelectionCriteria implements JsonSerializable
{
final public const AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE = null;
final public const AUTHENTICATOR_ATTACHMENT_PLATFORM = 'platform';
final public const AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 'cross-platform';
final public const AUTHENTICATOR_ATTACHMENTS = [
self::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE,
self::AUTHENTICATOR_ATTACHMENT_PLATFORM,
self::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM,
];
final public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
final public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
final public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
final public const USER_VERIFICATION_REQUIREMENTS = [
self::USER_VERIFICATION_REQUIREMENT_REQUIRED,
self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
self::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
];
final public const RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE = null;
/**
* @deprecated Please use AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE instead
* @infection-ignore-all
*/
final public const RESIDENT_KEY_REQUIREMENT_NONE = null;
final public const RESIDENT_KEY_REQUIREMENT_REQUIRED = 'required';
final public const RESIDENT_KEY_REQUIREMENT_PREFERRED = 'preferred';
final public const RESIDENT_KEY_REQUIREMENT_DISCOURAGED = 'discouraged';
final public const RESIDENT_KEY_REQUIREMENTS = [
self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
self::RESIDENT_KEY_REQUIREMENT_REQUIRED,
self::RESIDENT_KEY_REQUIREMENT_PREFERRED,
self::RESIDENT_KEY_REQUIREMENT_DISCOURAGED,
];
public function __construct(
public null|string $authenticatorAttachment = null,
public string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
public null|string $residentKey = self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
/** @deprecated Will be removed in 5.0. Please use residentKey instead**/
public null|bool $requireResidentKey = null,
) {
in_array($authenticatorAttachment, self::AUTHENTICATOR_ATTACHMENTS, true) || throw new InvalidArgumentException(
'Invalid authenticator attachment'
);
in_array($userVerification, self::USER_VERIFICATION_REQUIREMENTS, true) || throw new InvalidArgumentException(
'Invalid user verification'
);
in_array($residentKey, self::RESIDENT_KEY_REQUIREMENTS, true) || throw new InvalidArgumentException(
'Invalid resident key'
);
if ($requireResidentKey === true && $residentKey !== null && $residentKey !== self::RESIDENT_KEY_REQUIREMENT_REQUIRED) {
throw new InvalidArgumentException(
'Invalid resident key requirement. Resident key is required but requireResidentKey is false'
);
}
if ($this->residentKey === null && $this->requireResidentKey === true) {
$this->residentKey = self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
}
$this->requireResidentKey = $requireResidentKey ?? ($residentKey === null ? null : $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED);
}
public static function create(
?string $authenticatorAttachment = null,
string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
null|string $residentKey = self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE,
null|bool $requireResidentKey = null
): self {
return new self($authenticatorAttachment, $userVerification, $residentKey, $requireResidentKey);
}
/**
* @deprecated since 4.7.0. Please use the {self::create} instead.
* @infection-ignore-all
*/
public function setAuthenticatorAttachment(?string $authenticatorAttachment): self
{
$this->authenticatorAttachment = $authenticatorAttachment;
return $this;
}
/**
* @deprecated since v4.1. Please use the {self::create} instead.
* @infection-ignore-all
*/
public function setRequireResidentKey(bool $requireResidentKey): self
{
$this->requireResidentKey = $requireResidentKey;
if ($requireResidentKey === true) {
$this->residentKey = self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
}
return $this;
}
/**
* @deprecated since 4.7.0. Please use the {self::create} instead.
* @infection-ignore-all
*/
public function setUserVerification(string $userVerification): self
{
$this->userVerification = $userVerification;
return $this;
}
/**
* @deprecated since 4.7.0. Please use the {self::create} instead.
* @infection-ignore-all
*/
public function setResidentKey(null|string $residentKey): self
{
$this->residentKey = $residentKey;
$this->requireResidentKey = $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
return $this;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getAuthenticatorAttachment(): ?string
{
return $this->authenticatorAttachment;
}
/**
* @deprecated Will be removed in 5.0. Please use the property directly.
* @infection-ignore-all
*/
public function isRequireResidentKey(): bool
{
return $this->requireResidentKey;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getUserVerification(): string
{
return $this->userVerification;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getResidentKey(): null|string
{
return $this->residentKey;
}
/**
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
* @infection-ignore-all
*/
public static function createFromString(string $data): self
{
$data = json_decode($data, true, flags: JSON_THROW_ON_ERROR);
return self::createFromArray($data);
}
/**
* @param mixed[] $json
* @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object.
* @infection-ignore-all
*/
public static function createFromArray(array $json): self
{
$authenticatorAttachment = $json['authenticatorAttachment'] ?? null;
$requireResidentKey = $json['requireResidentKey'] ?? null;
$userVerification = $json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED;
$residentKey = $json['residentKey'] ?? null;
$authenticatorAttachment === null || is_string($authenticatorAttachment) || throw InvalidDataException::create(
$json,
'Invalid "authenticatorAttachment" value'
);
($requireResidentKey === null || is_bool($requireResidentKey)) || throw InvalidDataException::create(
$json,
'Invalid "requireResidentKey" value'
);
is_string($userVerification) || throw InvalidDataException::create($json, 'Invalid "userVerification" value');
($residentKey === null || is_string($residentKey)) || throw InvalidDataException::create(
$json,
'Invalid "residentKey" value'
);
return self::create(
$authenticatorAttachment ?? null,
$userVerification,
$residentKey,
$requireResidentKey,
);
}
/**
* @return mixed[]
*/
public function jsonSerialize(): array
{
trigger_deprecation(
'web-auth/webauthn-bundle',
'4.9.0',
'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.',
__METHOD__
);
$json = [
'requireResidentKey' => $this->requireResidentKey,
'userVerification' => $this->userVerification,
'residentKey' => $this->residentKey,
'authenticatorAttachment' => $this->authenticatorAttachment,
];
foreach ($json as $key => $value) {
if ($value === null) {
unset($json[$key]);
}
}
return $json;
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
interface CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void;
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CeremonyStepManager
{
/**
* @param CeremonyStep[] $steps
*/
public function __construct(
private readonly array $steps
) {
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
foreach ($this->steps as $step) {
$step->process(
$publicKeyCredentialSource,
$authenticatorResponse,
$publicKeyCredentialOptions,
$userHandle,
$host
);
}
}
}
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\RSA\RS256;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\Counter\CounterChecker;
use Webauthn\Counter\ThrowExceptionIfInvalid;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\StatusReportRepository;
final class CeremonyStepManagerFactory
{
private CounterChecker $counterChecker;
private Manager $algorithmManager;
private null|MetadataStatementRepository $metadataStatementRepository = null;
private null|StatusReportRepository $statusReportRepository = null;
private null|CertificateChainValidator $certificateChainValidator = null;
private null|TopOriginValidator $topOriginValidator = null;
/**
* @var string[]
*/
private null|array $securedRelyingPartyId = null;
private AttestationStatementSupportManager $attestationStatementSupportManager;
private ExtensionOutputCheckerHandler $extensionOutputCheckerHandler;
public function __construct()
{
$this->counterChecker = new ThrowExceptionIfInvalid();
$this->algorithmManager = Manager::create()->add(ES256::create(), RS256::create());
$this->attestationStatementSupportManager = new AttestationStatementSupportManager([
new NoneAttestationStatementSupport(),
]);
$this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler();
}
public function setCounterChecker(CounterChecker $counterChecker): void
{
$this->counterChecker = $counterChecker;
}
/**
* @param string[] $securedRelyingPartyId
*/
public function setSecuredRelyingPartyId(array $securedRelyingPartyId): void
{
$this->securedRelyingPartyId = $securedRelyingPartyId;
}
public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void
{
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
}
public function setAttestationStatementSupportManager(
AttestationStatementSupportManager $attestationStatementSupportManager
): void {
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
}
public function setAlgorithmManager(Manager $algorithmManager): void
{
$this->algorithmManager = $algorithmManager;
}
public function enableMetadataStatementSupport(
MetadataStatementRepository $metadataStatementRepository,
StatusReportRepository $statusReportRepository,
CertificateChainValidator $certificateChainValidator
): void {
$this->metadataStatementRepository = $metadataStatementRepository;
$this->statusReportRepository = $statusReportRepository;
$this->certificateChainValidator = $certificateChainValidator;
}
public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void
{
$this->certificateChainValidator = $certificateChainValidator;
}
public function enableTopOriginValidator(TopOriginValidator $topOriginValidator): void
{
$this->topOriginValidator = $topOriginValidator;
}
/**
* @param null|string[] $securedRelyingPartyId
*/
public function creationCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager
{
$metadataStatementChecker = new CheckMetadataStatement();
if ($this->certificateChainValidator !== null) {
$metadataStatementChecker->enableCertificateChainValidator($this->certificateChainValidator);
}
if ($this->metadataStatementRepository !== null && $this->statusReportRepository !== null && $this->certificateChainValidator !== null) {
$metadataStatementChecker->enableMetadataStatementSupport(
$this->metadataStatementRepository,
$this->statusReportRepository,
$this->certificateChainValidator,
);
}
/* @see https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential */
return new CeremonyStepManager([
new CheckClientDataCollectorType(),
new CheckChallenge(),
new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []),
new CheckTopOrigin($this->topOriginValidator),
new CheckRelyingPartyIdIdHash(),
new CheckUserWasPresent(),
new CheckUserVerification(),
new CheckBackupBitsAreConsistent(),
new CheckAlgorithm(),
new CheckExtensions($this->extensionOutputCheckerHandler),
new CheckAttestationFormatIsKnownAndValid($this->attestationStatementSupportManager),
new CheckHasAttestedCredentialData(),
$metadataStatementChecker,
new CheckCredentialId(),
]);
}
/**
* @param null|string[] $securedRelyingPartyId
*/
public function requestCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager
{
/* @see https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion */
return new CeremonyStepManager([
new CheckAllowedCredentialList(),
new CheckUserHandle(),
new CheckClientDataCollectorType(),
new CheckChallenge(),
new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []),
new CheckTopOrigin(null),
new CheckRelyingPartyIdIdHash(),
new CheckUserWasPresent(),
new CheckUserVerification(),
new CheckBackupBitsAreConsistent(),
new CheckExtensions($this->extensionOutputCheckerHandler),
new CheckSignature($this->algorithmManager),
new CheckCounter($this->counterChecker),
]);
}
}
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use CBOR\Decoder;
use CBOR\Normalizable;
use Cose\Algorithms;
use Cose\Key\Key;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\StringStream;
use Webauthn\U2FPublicKey;
use function count;
use function in_array;
use function is_array;
class CheckAlgorithm implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
if (! $publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions) {
return;
}
$credentialPublicKey = $publicKeyCredentialSource->getAttestedCredentialData()
->credentialPublicKey;
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
'No public key available.'
);
$algorithms = array_map(
fn ($pubKeyCredParam) => $pubKeyCredParam->alg,
$publicKeyCredentialOptions->pubKeyCredParams
);
if (count($algorithms) === 0) {
$algorithms = [Algorithms::COSE_ALGORITHM_ES256, Algorithms::COSE_ALGORITHM_RS256];
}
$coseKey = $this->getCoseKey($credentialPublicKey);
in_array($coseKey->alg(), $algorithms, true) || throw AuthenticatorResponseVerificationException::create(
sprintf('Invalid algorithm. Expected one of %s but got %d', implode(', ', $algorithms), $coseKey->alg())
);
}
private function getCoseKey(string $credentialPublicKey): Key
{
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
if ($isU2F === true) {
$credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey);
}
$stream = new StringStream($credentialPublicKey);
$credentialPublicKeyStream = Decoder::create()->decode($stream);
$stream->isEOF() || throw AuthenticatorResponseVerificationException::create(
'Invalid key. Presence of extra bytes.'
);
$stream->close();
$credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create(
'Invalid attestation object. Unexpected object.'
);
$normalizedData = $credentialPublicKeyStream->normalize();
is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create(
'Invalid attestation object. Unexpected object.'
);
/** @var array<int|string, mixed> $normalizedData */
return Key::create($normalizedData);
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use function count;
final class CheckAllowedCredentialList implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
if (! $publicKeyCredentialOptions instanceof PublicKeyCredentialRequestOptions) {
return;
}
if (count($publicKeyCredentialOptions->allowCredentials) === 0) {
return;
}
foreach ($publicKeyCredentialOptions->allowCredentials as $allowedCredential) {
if (hash_equals($allowedCredential->id, $publicKeyCredentialSource->publicKeyCredentialId)) {
return;
}
}
throw AuthenticatorResponseVerificationException::create('The credential ID is not allowed.');
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckAttestationFormatIsKnownAndValid implements CeremonyStep
{
public function __construct(
private readonly AttestationStatementSupportManager $attestationStatementSupportManager,
) {
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$attestationObject = $authenticatorResponse->attestationObject;
if ($attestationObject === null) {
return;
}
$fmt = $attestationObject->attStmt
->fmt;
$this->attestationStatementSupportManager->has(
$fmt
) || throw AuthenticatorResponseVerificationException::create('Unsupported attestation statement format.');
$attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
$clientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON ->rawData, true);
$attestationStatementSupport->isValid(
$clientDataJSONHash,
$attestationObject->attStmt,
$attestationObject->authData
) || throw AuthenticatorResponseVerificationException::create('Invalid attestation statement.');
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckBackupBitsAreConsistent implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
if ($authData->isBackupEligible()) {
return;
}
$authData->isBackedUp() !== true || throw AuthenticatorResponseVerificationException::create(
'Backup up bit is set but the backup is not eligible.'
);
}
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckChallenge implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$publicKeyCredentialOptions->challenge !== '' || throw AuthenticatorResponseVerificationException::create(
'Invalid challenge.'
);
hash_equals(
$publicKeyCredentialOptions->challenge,
$authenticatorResponse->clientDataJSON->challenge
) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.');
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\ClientDataCollector\ClientDataCollectorManager;
use Webauthn\ClientDataCollector\WebauthnAuthenticationCollector;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckClientDataCollectorType implements CeremonyStep
{
private readonly ClientDataCollectorManager $clientDataCollectorManager;
public function __construct(
null|ClientDataCollectorManager $clientDataCollectorManager = null,
) {
$this->clientDataCollectorManager = $clientDataCollectorManager ?? new ClientDataCollectorManager([
new WebauthnAuthenticationCollector(),
]);
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$this->clientDataCollectorManager->collect(
$authenticatorResponse->clientDataJSON,
$publicKeyCredentialOptions,
$authenticatorResponse,
$host
);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Counter\CounterChecker;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckCounter implements CeremonyStep
{
public function __construct(
private readonly CounterChecker $counterChecker
) {
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$storedCounter = $publicKeyCredentialSource->counter;
$responseCounter = $authData->signCount;
if ($responseCounter !== 0 || $storedCounter !== 0) {
$this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
}
$publicKeyCredentialSource->counter = $responseCounter;
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
class CheckCredentialId implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$credentialId = $publicKeyCredentialSource->publicKeyCredentialId;
mb_strlen($credentialId) <= 1023 || throw new AuthenticatorResponseVerificationException(
'Credential ID too long.'
);
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckExtensions implements CeremonyStep
{
public function __construct(
private readonly ExtensionOutputCheckerHandler $extensionOutputCheckerHandler,
) {
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$extensionsClientOutputs = $authData->extensions;
if ($extensionsClientOutputs !== null) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialOptions->extensions,
$extensionsClientOutputs
);
}
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckHasAttestedCredentialData implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$authData
->hasAttestedCredentialData() || throw AuthenticatorResponseVerificationException::create(
'There is no attested credential data.'
);
$authData->attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
'There is no attested credential data.'
);
}
}
@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Webauthn\AttestationStatement\AttestationStatement;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\MetadataService\CanLogData;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\Statement\MetadataStatement;
use Webauthn\MetadataService\StatusReportRepository;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\TrustPath\CertificateTrustPath;
use function count;
use function in_array;
final class CheckMetadataStatement implements CeremonyStep, CanLogData
{
private LoggerInterface $logger;
private null|MetadataStatementRepository $metadataStatementRepository = null;
private null|StatusReportRepository $statusReportRepository = null;
private null|CertificateChainValidator $certificateChainValidator = null;
public function __construct()
{
$this->logger = new NullLogger();
}
public function enableMetadataStatementSupport(
MetadataStatementRepository $metadataStatementRepository,
StatusReportRepository $statusReportRepository,
CertificateChainValidator $certificateChainValidator
): void {
$this->metadataStatementRepository = $metadataStatementRepository;
$this->statusReportRepository = $statusReportRepository;
$this->certificateChainValidator = $certificateChainValidator;
}
public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void
{
$this->certificateChainValidator = $certificateChainValidator;
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
if (
! $publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions
|| ! $authenticatorResponse instanceof AuthenticatorAttestationResponse
) {
return;
}
$attestationStatement = $authenticatorResponse->attestationObject->attStmt;
$attestedCredentialData = $authenticatorResponse->attestationObject->authData
->attestedCredentialData;
$attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
'No attested credential data found'
);
$aaguid = $attestedCredentialData->aaguid
->__toString();
if ($publicKeyCredentialOptions->attestation === null || $publicKeyCredentialOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) {
$this->logger->debug('No attestation is asked.');
if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array(
$attestationStatement->type,
[AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF],
true
)) {
$this->logger->debug('The Attestation Statement is anonymous.');
$this->checkCertificateChain($attestationStatement, null);
return;
}
return;
}
// If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
// => nothing to check
if ($attestationStatement->type === AttestationStatement::TYPE_NONE) {
$this->logger->debug('No attestation returned.');
//No attestation is returned. We shall ensure that the AAGUID is a null one.
//if ($aaguid !== '00000000-0000-0000-0000-000000000000') {
//$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
// 'aaguid' => $aaguid,
// 'AttestationStatement' => $attestationStatement,
//]);
//$attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000');
// return;
//}
return;
}
if ($aaguid === '00000000-0000-0000-0000-000000000000') {
//No need to continue if the AAGUID is null.
// This could be the case e.g. with AnonCA type
return;
}
//The MDS Repository is mandatory here
$this->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create(
'The Metadata Statement Repository is mandatory when requesting attestation objects.'
);
$metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
// At this point, the Metadata Statement is mandatory
$metadataStatement !== null || throw AuthenticatorResponseVerificationException::create(
sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid)
);
// We check the last status report
$this->checkStatusReport($aaguid);
// We check the certificate chain (if any)
$this->checkCertificateChain($attestationStatement, $metadataStatement);
// Check Attestation Type is allowed
if (count($metadataStatement->attestationTypes) !== 0) {
$type = $this->getAttestationType($attestationStatement);
in_array(
$type,
$metadataStatement->attestationTypes,
true
) || throw AuthenticatorResponseVerificationException::create(
sprintf(
'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator.',
$type
)
);
}
}
private function getAttestationType(AttestationStatement $attestationStatement): string
{
return match ($attestationStatement->type) {
AttestationStatement::TYPE_BASIC => MetadataStatement::ATTESTATION_BASIC_FULL,
AttestationStatement::TYPE_SELF => MetadataStatement::ATTESTATION_BASIC_SURROGATE,
AttestationStatement::TYPE_ATTCA => MetadataStatement::ATTESTATION_ATTCA,
AttestationStatement::TYPE_ECDAA => MetadataStatement::ATTESTATION_ECDAA,
AttestationStatement::TYPE_ANONCA => MetadataStatement::ATTESTATION_ANONCA,
default => throw AuthenticatorResponseVerificationException::create('Invalid attestation type'),
};
}
private function checkStatusReport(string $aaguid): void
{
$statusReports = $this->statusReportRepository === null ? [] : $this->statusReportRepository->findStatusReportsByAAGUID(
$aaguid
);
if (count($statusReports) !== 0) {
$lastStatusReport = end($statusReports);
if ($lastStatusReport->isCompromised()) {
throw AuthenticatorResponseVerificationException::create(
'The authenticator is compromised and cannot be used'
);
}
}
}
private function checkCertificateChain(
AttestationStatement $attestationStatement,
?MetadataStatement $metadataStatement
): void {
$trustPath = $attestationStatement->trustPath;
if (! $trustPath instanceof CertificateTrustPath) {
return;
}
$authenticatorCertificates = $trustPath->certificates;
if ($metadataStatement === null) {
$this->certificateChainValidator?->check($authenticatorCertificates, []);
return;
}
$trustedCertificates = CertificateToolbox::fixPEMStructures(
$metadataStatement->attestationRootCertificates
);
$this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates);
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use function in_array;
use function is_array;
use function is_string;
final class CheckOrigin implements CeremonyStep
{
/**
* @param string[] $securedRelyingPartyId
*/
public function __construct(
private readonly array $securedRelyingPartyId
) {
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$C = $authenticatorResponse->clientDataJSON;
$rpId = $publicKeyCredentialOptions->rpId ?? $publicKeyCredentialOptions->rp->id ?? $host;
$facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData->extensions);
$parsedRelyingPartyId = parse_url($C->origin);
is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create(
'Invalid origin'
);
if (! in_array($facetId, $this->securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
$scheme === 'https' || throw AuthenticatorResponseVerificationException::create(
'Invalid scheme. HTTPS required.'
);
}
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
$clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
mb_substr(
'.' . $clientDataRpId,
-($rpIdLength + 1)
) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.');
}
private function getFacetId(
string $rpId,
AuthenticationExtensions $authenticationExtensionsClientInputs,
null|AuthenticationExtensions $authenticationExtensionsClientOutputs
): string {
if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has(
'appid'
) || ! $authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')
->value;
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')
->value;
if (! is_string($appId) || $wasUsed !== true) {
return $rpId;
}
return $appId;
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticationExtensions\AuthenticationExtensions;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\U2FPublicKey;
use function is_string;
final class CheckRelyingPartyIdIdHash implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$C = $authenticatorResponse->clientDataJSON;
$attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
$credentialPublicKey = $attestedCredentialData->credentialPublicKey;
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
'No public key available.'
);
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
$rpId = $publicKeyCredentialOptions->rpId ?? $publicKeyCredentialOptions->rp->id ?? $host;
$facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData ->extensions);
$rpIdHash = hash('sha256', $isU2F ? $C->origin : $facetId, true);
hash_equals(
$rpIdHash,
$authData
->rpIdHash
) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.');
}
private function getFacetId(
string $rpId,
AuthenticationExtensions $authenticationExtensionsClientInputs,
null|AuthenticationExtensions $authenticationExtensionsClientOutputs
): string {
if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has(
'appid'
) || ! $authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')
->value;
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')
->value;
if (! is_string($appId) || $wasUsed !== true) {
return $rpId;
}
return $appId;
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use CBOR\Decoder;
use CBOR\Normalizable;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\ECDSA\ES256;
use Cose\Algorithm\Signature\RSA\RS256;
use Cose\Algorithm\Signature\Signature;
use Cose\Key\Key;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\StringStream;
use Webauthn\U2FPublicKey;
use Webauthn\Util\CoseSignatureFixer;
use function is_array;
final class CheckSignature implements CeremonyStep
{
private readonly Manager $algorithmManager;
public function __construct(
null|Manager $algorithmManager = null,
) {
$this->algorithmManager = $algorithmManager ?? Manager::create()->add(ES256::create(), RS256::create());
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
if (! $authenticatorResponse instanceof AuthenticatorAssertionResponse) {
return;
}
$credentialPublicKey = $publicKeyCredentialSource->getAttestedCredentialData()
->credentialPublicKey;
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
'No public key available.'
);
$coseKey = $this->getCoseKey($credentialPublicKey);
$getClientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON->rawData, true);
$dataToVerify = $authenticatorResponse->authenticatorData->authData . $getClientDataJSONHash;
$signature = $authenticatorResponse->signature;
$algorithm = $this->algorithmManager->get($coseKey->alg());
$algorithm instanceof Signature || throw AuthenticatorResponseVerificationException::create(
'Invalid algorithm identifier. Should refer to a signature algorithm'
);
$signature = CoseSignatureFixer::fix($signature, $algorithm);
$algorithm->verify(
$dataToVerify,
$coseKey,
$signature
) || throw AuthenticatorResponseVerificationException::create('Invalid signature.');
}
private function getCoseKey(string $credentialPublicKey): Key
{
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
if ($isU2F === true) {
$credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey);
}
$stream = new StringStream($credentialPublicKey);
$credentialPublicKeyStream = Decoder::create()->decode($stream);
$stream->isEOF() || throw AuthenticatorResponseVerificationException::create(
'Invalid key. Presence of extra bytes.'
);
$stream->close();
$credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create(
'Invalid attestation object. Unexpected object.'
);
$normalizedData = $credentialPublicKeyStream->normalize();
is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create(
'Invalid attestation object. Unexpected object.'
);
/** @var array<int|string, mixed> $normalizedData */
return Key::create($normalizedData);
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
class CheckTopOrigin implements CeremonyStep
{
public function __construct(
private readonly null|TopOriginValidator $topOriginValidator = null
) {
}
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$topOrigin = $authenticatorResponse->clientDataJSON->topOrigin;
if ($topOrigin === null) {
return;
}
if ($authenticatorResponse->clientDataJSON->crossOrigin !== true) {
throw AuthenticatorResponseVerificationException::create('The response is not cross-origin.');
}
if ($this->topOriginValidator === null) {
(new HostTopOriginValidator($host))->validate($topOrigin);
} else {
$this->topOriginValidator->validate($topOrigin);
}
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\InvalidUserHandleException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckUserHandle implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
if (! $authenticatorResponse instanceof AuthenticatorAssertionResponse) {
return;
}
$credentialUserHandle = $publicKeyCredentialSource->userHandle;
$responseUserHandle = $authenticatorResponse->userHandle;
if ($userHandle !== null) { //If the user was identified before the authentication ceremony was initiated,
$credentialUserHandle === $userHandle || throw InvalidUserHandleException::create();
if ($responseUserHandle !== null && $responseUserHandle !== '') {
$credentialUserHandle === $responseUserHandle || throw InvalidUserHandleException::create();
}
} else {
($responseUserHandle !== '' && $credentialUserHandle === $responseUserHandle) || throw InvalidUserHandleException::create();
}
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckUserVerification implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$userVerification = $publicKeyCredentialOptions instanceof PublicKeyCredentialRequestOptions ? $publicKeyCredentialOptions->userVerification : $publicKeyCredentialOptions->authenticatorSelection?->userVerification;
if ($userVerification !== AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) {
return;
}
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$authData->isUserVerified() || throw AuthenticatorResponseVerificationException::create(
'User authentication required.'
);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\AuthenticatorAssertionResponse;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialSource;
final class CheckUserWasPresent implements CeremonyStep
{
public function process(
PublicKeyCredentialSource $publicKeyCredentialSource,
AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse,
PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions,
?string $userHandle,
string $host
): void {
$authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData;
$authData->isUserPresent() || throw AuthenticatorResponseVerificationException::create('User was not present');
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
final class HostTopOriginValidator implements TopOriginValidator
{
public function __construct(
private readonly string $host
) {
}
public function validate(string $topOrigin): void
{
$topOrigin === $this->host || throw AuthenticatorResponseVerificationException::create(
'The top origin does not correspond to the host.'
);
}
}
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Webauthn\CeremonyStep;
interface TopOriginValidator
{
public function validate(string $topOrigin): void;
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\CertificateChainChecker;
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
/**
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\CertificateChainValidator instead
* @infection-ignore-all
*/
interface CertificateChainChecker extends CertificateChainValidator
{
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn\CertificateChainChecker;
use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator;
/**
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\PhpCertificateChainValidator instead
* @infection-ignore-all
*/
final class PhpCertificateChainChecker extends PhpCertificateChainValidator
{
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use Webauthn\MetadataService\CertificateChain\CertificateToolbox as BaseCertificateToolbox;
/**
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\PhpCertificateChainValidator instead
* @infection-ignore-all
*/
class CertificateToolbox extends BaseCertificateToolbox
{
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Webauthn\ClientDataCollector;
use Webauthn\AuthenticatorResponse;
use Webauthn\CollectedClientData;
use Webauthn\PublicKeyCredentialOptions;
interface ClientDataCollector
{
/**
* @return string[]
*/
public function supportedTypes(): array;
public function verifyCollectedClientData(
CollectedClientData $collectedClientData,
PublicKeyCredentialOptions $publicKeyCredentialOptions,
AuthenticatorResponse $authenticatorResponse,
string $host
): void;
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Webauthn\ClientDataCollector;
use Webauthn\AuthenticatorResponse;
use Webauthn\CollectedClientData;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialOptions;
use function in_array;
final class ClientDataCollectorManager
{
/**
* @param ClientDataCollector[] $clientDataCollectors
*/
public function __construct(
private readonly iterable $clientDataCollectors,
) {
}
public function collect(
CollectedClientData $collectedClientData,
PublicKeyCredentialOptions $publicKeyCredentialOptions,
AuthenticatorResponse $authenticatorResponse,
string $host
): void {
foreach ($this->clientDataCollectors as $clientDataCollector) {
if (in_array($collectedClientData->type, $clientDataCollector->supportedTypes(), true)) {
$clientDataCollector->verifyCollectedClientData(
$collectedClientData,
$publicKeyCredentialOptions,
$authenticatorResponse,
$host
);
return;
}
}
throw AuthenticatorResponseVerificationException::create('No client data collector found.');
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Webauthn\ClientDataCollector;
use Webauthn\AuthenticatorResponse;
use Webauthn\CollectedClientData;
use Webauthn\Exception\AuthenticatorResponseVerificationException;
use Webauthn\PublicKeyCredentialOptions;
use function in_array;
final class WebauthnAuthenticationCollector implements ClientDataCollector
{
public function supportedTypes(): array
{
return ['webauthn.get', 'webauthn.create'];
}
public function verifyCollectedClientData(
CollectedClientData $collectedClientData,
PublicKeyCredentialOptions $publicKeyCredentialOptions,
AuthenticatorResponse $authenticatorResponse,
string $host
): void {
in_array(
$collectedClientData->type,
$this->supportedTypes(),
true
) || throw AuthenticatorResponseVerificationException::create(
sprintf('The client data type is not "%s" supported.', implode('", "', $this->supportedTypes()))
);
}
}
@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Webauthn;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Webauthn\Exception\InvalidDataException;
use Webauthn\TokenBinding\TokenBinding;
use function array_key_exists;
use function is_array;
use function is_string;
use const JSON_THROW_ON_ERROR;
class CollectedClientData
{
/**
* @var mixed[]
*/
public readonly array $data;
public readonly string $type;
public readonly string $challenge;
public readonly string $origin;
public readonly null|string $topOrigin;
public readonly bool $crossOrigin;
/**
* @var mixed[]|null
* @deprecated Since 4.3.0 and will be removed in 5.0.0
* @infection-ignore-all
*/
public readonly ?array $tokenBinding;
/**
* @param mixed[] $data
*/
public function __construct(
public readonly string $rawData,
array $data
) {
$type = $data['type'] ?? '';
(is_string($type) && $type !== '') || throw InvalidDataException::create(
$data,
'Invalid parameter "type". Shall be a non-empty string.'
);
$this->type = $type;
$challenge = $data['challenge'] ?? '';
is_string($challenge) || throw InvalidDataException::create(
$data,
'Invalid parameter "challenge". Shall be a string.'
);
$challenge = Base64UrlSafe::decodeNoPadding($challenge);
$challenge !== '' || throw InvalidDataException::create(
$data,
'Invalid parameter "challenge". Shall not be empty.'
);
$this->challenge = $challenge;
$origin = $data['origin'] ?? '';
(is_string($origin) && $origin !== '') || throw InvalidDataException::create(
$data,
'Invalid parameter "origin". Shall be a non-empty string.'
);
$this->origin = $origin;
$this->topOrigin = $data['topOrigin'] ?? null;
$this->crossOrigin = $data['crossOrigin'] ?? false;
$tokenBinding = $data['tokenBinding'] ?? null;
$tokenBinding === null || is_array($tokenBinding) || throw InvalidDataException::create(
$data,
'Invalid parameter "tokenBinding". Shall be an object or .'
);
$this->tokenBinding = $tokenBinding;
$this->data = $data;
}
/**
* @param mixed[] $data
*/
public static function create(string $rawData, array $data): self
{
return new self($rawData, $data);
}
public static function createFormJson(string $data): self
{
$rawData = Base64UrlSafe::decodeNoPadding($data);
$json = json_decode($rawData, true, flags: JSON_THROW_ON_ERROR);
is_array($json) || throw InvalidDataException::create($data, 'Invalid JSON data.');
return self::create($rawData, $json);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getType(): string
{
return $this->type;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getChallenge(): string
{
return $this->challenge;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getOrigin(): string
{
return $this->origin;
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getCrossOrigin(): bool
{
return $this->crossOrigin;
}
/**
* @deprecated Since 4.3.0 and will be removed in 5.0.0
* @infection-ignore-all
*/
public function getTokenBinding(): ?TokenBinding
{
return $this->tokenBinding === null ? null : TokenBinding::createFormArray($this->tokenBinding);
}
/**
* @deprecated since 4.7.0. Please use the property directly.
* @infection-ignore-all
*/
public function getRawData(): string
{
return $this->rawData;
}
/**
* @return string[]
*/
public function all(): array
{
return array_keys($this->data);
}
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
public function get(string $key): mixed
{
if (! $this->has($key)) {
throw InvalidDataException::create($this->data, sprintf('The key "%s" is missing', $key));
}
return $this->data[$key];
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Webauthn\Counter;
use Webauthn\PublicKeyCredentialSource;
interface CounterChecker
{
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void;
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Webauthn\Counter;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Webauthn\Exception\CounterException;
use Webauthn\MetadataService\CanLogData;
use Webauthn\PublicKeyCredentialSource;
final class ThrowExceptionIfInvalid implements CounterChecker, CanLogData
{
public function __construct(
private LoggerInterface $logger = new NullLogger()
) {
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void
{
try {
$currentCounter > $publicKeyCredentialSource->counter || throw CounterException::create(
$currentCounter,
$publicKeyCredentialSource->counter,
'Invalid counter.'
);
} catch (CounterException $throwable) {
$this->logger->error('The counter is invalid', [
'current' => $currentCounter,
'new' => $publicKeyCredentialSource->counter,
]);
throw $throwable;
}
}
}
+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);
}
}

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