first commit
This commit is contained in:
21
libraries/vendor/web-auth/webauthn-lib/LICENSE
vendored
Normal file
21
libraries/vendor/web-auth/webauthn-lib/LICENSE
vendored
Normal 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,231 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function openssl_pkey_get_public;
|
||||
use function openssl_verify;
|
||||
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\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @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'],
|
||||
new CertificateTrustPath($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->getTrustPath();
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path. Shall contain certificates.'
|
||||
);
|
||||
|
||||
$certificates = $trustPath->getCertificates();
|
||||
|
||||
//Decode leaf attestation certificate
|
||||
$leaf = $certificates[0];
|
||||
$this->checkCertificate($leaf, $clientDataJSONHash, $authenticatorData);
|
||||
|
||||
$signedData = $authenticatorData->getAuthData() . $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->getAttestedCredentialData();
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential data found'
|
||||
);
|
||||
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
|
||||
$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,359 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
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 const JSON_THROW_ON_ERROR;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\Exception\UnsupportedFeatureException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
|
||||
{
|
||||
private ?string $apiKey = null;
|
||||
|
||||
private ?ClientInterface $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()
|
||||
{
|
||||
if (! class_exists(RS256::class)) {
|
||||
throw UnsupportedFeatureException::create(
|
||||
'The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?'
|
||||
);
|
||||
}
|
||||
if (! class_exists(JWKFactory::class)) {
|
||||
throw UnsupportedFeatureException::create(
|
||||
'The class Jose\Component\KeyManagement\JWKFactory is missing. Did you forget to install the package web-token/jwt-key-mgmt?'
|
||||
);
|
||||
}
|
||||
$this->jwsSerializer = new CompactSerializer();
|
||||
$this->initJwsVerifier();
|
||||
$this->dispatcher = new NullEventDispatcher();
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->dispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function enableApiVerification(
|
||||
ClientInterface $client,
|
||||
string $apiKey,
|
||||
RequestFactoryInterface $requestFactory
|
||||
): self {
|
||||
$this->apiKey = $apiKey;
|
||||
$this->client = $client;
|
||||
$this->requestFactory = $requestFactory;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMaxAge(int $maxAge): self
|
||||
{
|
||||
$this->maxAge = $maxAge;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLeeway(int $leeway): self
|
||||
{
|
||||
$this->leeway = $leeway;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'android-safetynet';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create(
|
||||
$attestation,
|
||||
'Invalid attestation object'
|
||||
);
|
||||
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'],
|
||||
new CertificateTrustPath($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->getTrustPath();
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path'
|
||||
);
|
||||
$certificates = $trustPath->getCertificates();
|
||||
$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, 512, 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->getAuthData() . $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 = 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->getCertificates()[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 || $this->requestFactory === null) {
|
||||
return;
|
||||
}
|
||||
$uri = sprintf(
|
||||
'https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s',
|
||||
urlencode($this->apiKey)
|
||||
);
|
||||
$requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
|
||||
$request = $this->requestFactory->createRequest('POST', $uri);
|
||||
$request = $request->withHeader('content-type', 'application/json');
|
||||
$request->getBody()
|
||||
->write($requestBody);
|
||||
|
||||
$response = $this->client->sendRequest($request);
|
||||
$this->checkGoogleApiResponse($response);
|
||||
$responseBody = $this->getResponseBody($response);
|
||||
$responseBodyJson = json_decode($responseBody, true, 512, 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;
|
||||
}
|
||||
|
||||
private function checkGoogleApiResponse(ResponseInterface $response): void
|
||||
{
|
||||
$response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create(
|
||||
'Request did not succeeded'
|
||||
);
|
||||
$response->hasHeader('content-type') || throw AttestationStatementVerificationException::create(
|
||||
'Unrecognized response'
|
||||
);
|
||||
|
||||
foreach ($response->getHeader('content-type') as $header) {
|
||||
if (mb_strpos($header, 'application/json') === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw AttestationStatementVerificationException::create('Unrecognized response');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $certificates
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function convertCertificatesToPem(array $certificates): array
|
||||
{
|
||||
foreach ($certificates as $k => $v) {
|
||||
$certificates[$k] = CertificateToolbox::fixPEMStructure($v);
|
||||
}
|
||||
|
||||
return $certificates;
|
||||
}
|
||||
|
||||
private function initJwsVerifier(): void
|
||||
{
|
||||
$algorithmClasses = [
|
||||
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)) {
|
||||
/** @var AlgorithmInterface $algorithm */
|
||||
$algorithms[] = new $algorithm();
|
||||
}
|
||||
}
|
||||
$algorithmManager = new AlgorithmManager($algorithms);
|
||||
$this->jwsVerifier = new JWSVerifier($algorithmManager);
|
||||
}
|
||||
}
|
||||
177
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php
vendored
Normal file
177
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function openssl_pkey_get_public;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
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'],
|
||||
new CertificateTrustPath($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$trustPath = $attestationStatement->getTrustPath();
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path'
|
||||
);
|
||||
|
||||
$certificates = $trustPath->getCertificates();
|
||||
|
||||
//Decode leaf attestation certificate
|
||||
$leaf = $certificates[0];
|
||||
|
||||
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function checkCertificateAndGetPublicKey(
|
||||
string $certificate,
|
||||
string $clientDataHash,
|
||||
AuthenticatorData $authenticatorData
|
||||
): void {
|
||||
$resource = openssl_pkey_get_public($certificate);
|
||||
$details = openssl_pkey_get_details($resource);
|
||||
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->getAttestedCredentialData();
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential data found'
|
||||
);
|
||||
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
|
||||
$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->getAuthData() . $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');
|
||||
}
|
||||
}
|
||||
52
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php
vendored
Normal file
52
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\MetadataService\Statement\MetadataStatement;
|
||||
|
||||
class AttestationObject
|
||||
{
|
||||
private ?MetadataStatement $metadataStatement = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $rawAttestationObject,
|
||||
private AttestationStatement $attStmt,
|
||||
private readonly AuthenticatorData $authData
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRawAttestationObject(): string
|
||||
{
|
||||
return $this->rawAttestationObject;
|
||||
}
|
||||
|
||||
public function getAttStmt(): AttestationStatement
|
||||
{
|
||||
return $this->attStmt;
|
||||
}
|
||||
|
||||
public function setAttStmt(AttestationStatement $attStmt): void
|
||||
{
|
||||
$this->attStmt = $attStmt;
|
||||
}
|
||||
|
||||
public function getAuthData(): AuthenticatorData
|
||||
{
|
||||
return $this->authData;
|
||||
}
|
||||
|
||||
public function getMetadataStatement(): ?MetadataStatement
|
||||
{
|
||||
return $this->metadataStatement;
|
||||
}
|
||||
|
||||
public function setMetadataStatement(MetadataStatement $metadataStatement): self
|
||||
{
|
||||
$this->metadataStatement = $metadataStatement;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
180
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php
vendored
Normal file
180
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php
vendored
Normal file
@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use CBOR\Normalizable;
|
||||
use function is_array;
|
||||
use function ord;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Throwable;
|
||||
use function unpack;
|
||||
use Webauthn\AttestedCredentialData;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationObjectLoaded;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\Util\Base64;
|
||||
|
||||
class AttestationObjectLoader implements CanDispatchEvents, CanLogData
|
||||
{
|
||||
private const FLAG_AT = 0b01000000;
|
||||
|
||||
private const FLAG_ED = 0b10000000;
|
||||
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private EventDispatcherInterface $dispatcher;
|
||||
|
||||
public function __construct(
|
||||
private readonly AttestationStatementSupportManager $attestationStatementSupportManager
|
||||
) {
|
||||
$this->decoder = Decoder::create();
|
||||
$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 = $this->decoder->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'
|
||||
);
|
||||
$authData = $attestationObject['authData'];
|
||||
|
||||
$attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']);
|
||||
$attestationStatement = $attestationStatementSupport->load($attestationObject);
|
||||
$this->logger->info('Attestation Statement loaded');
|
||||
$this->logger->debug('Attestation Statement loaded', [
|
||||
'attestationStatement' => $attestationStatement,
|
||||
]);
|
||||
|
||||
$authDataStream = new StringStream($authData);
|
||||
$rp_id_hash = $authDataStream->read(32);
|
||||
$flags = $authDataStream->read(1);
|
||||
$signCount = $authDataStream->read(4);
|
||||
$signCount = unpack('N', $signCount);
|
||||
$this->logger->debug(sprintf('Signature counter: %d', $signCount[1]));
|
||||
|
||||
$attestedCredentialData = null;
|
||||
if (0 !== (ord($flags) & self::FLAG_AT)) {
|
||||
$this->logger->info('Attested Credential Data is present');
|
||||
$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(
|
||||
$credentialPublicKey,
|
||||
'The data does not contain a valid credential public key.'
|
||||
);
|
||||
$attestedCredentialData = new AttestedCredentialData(
|
||||
$aaguid,
|
||||
$credentialId,
|
||||
(string) $credentialPublicKey
|
||||
);
|
||||
$this->logger->info('Attested Credential Data loaded');
|
||||
$this->logger->debug('Attested Credential Data loaded', [
|
||||
'at' => $attestedCredentialData,
|
||||
]);
|
||||
}
|
||||
|
||||
$extension = null;
|
||||
if (0 !== (ord($flags) & self::FLAG_ED)) {
|
||||
$this->logger->info('Extension Data loaded');
|
||||
$extension = $this->decoder->decode($authDataStream);
|
||||
$extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
|
||||
$this->logger->info('Extension Data loaded');
|
||||
$this->logger->debug('Extension Data loaded', [
|
||||
'ed' => $extension,
|
||||
]);
|
||||
}
|
||||
$authDataStream->isEOF() || throw InvalidDataException::create(
|
||||
null,
|
||||
'Invalid authentication data. Presence of extra bytes.'
|
||||
);
|
||||
$authDataStream->close();
|
||||
|
||||
$authenticatorData = new AuthenticatorData(
|
||||
$authData,
|
||||
$rp_id_hash,
|
||||
$flags,
|
||||
$signCount[1],
|
||||
$attestedCredentialData,
|
||||
$extension
|
||||
);
|
||||
$attestationObject = new AttestationObject($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;
|
||||
}
|
||||
}
|
||||
161
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php
vendored
Normal file
161
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\TrustPath\TrustPath;
|
||||
use Webauthn\TrustPath\TrustPathLoader;
|
||||
|
||||
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.
|
||||
*/
|
||||
final public const TYPE_ECDAA = 'ecdaa';
|
||||
|
||||
final public const TYPE_ANONCA = 'anonca';
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $fmt,
|
||||
private readonly array $attStmt,
|
||||
private readonly string $type,
|
||||
private readonly TrustPath $trustPath
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_NONE, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_BASIC, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_SELF, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($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.
|
||||
*/
|
||||
public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_ECDAA, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attStmt
|
||||
*/
|
||||
public static function createAnonymizationCA(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_ANONCA, $trustPath);
|
||||
}
|
||||
|
||||
public function getFmt(): string
|
||||
{
|
||||
return $this->fmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getAttStmt(): array
|
||||
{
|
||||
return $this->attStmt;
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->attStmt);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
public function getTrustPath(): TrustPath
|
||||
{
|
||||
return $this->trustPath;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public static function createFromArray(array $data): self
|
||||
{
|
||||
foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) {
|
||||
array_key_exists($key, $data) || throw InvalidDataException::create($data, sprintf(
|
||||
'The key "%s" is missing',
|
||||
$key
|
||||
));
|
||||
}
|
||||
|
||||
return new self(
|
||||
$data['fmt'],
|
||||
$data['attStmt'],
|
||||
$data['type'],
|
||||
TrustPathLoader::loadTrustPath($data['trustPath'])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'fmt' => $this->fmt,
|
||||
'attStmt' => $this->attStmt,
|
||||
'trustPath' => $this->trustPath->jsonSerialize(),
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
}
|
||||
23
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php
vendored
Normal file
23
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php
vendored
Normal file
@ -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,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
class AttestationStatementSupportManager
|
||||
{
|
||||
/**
|
||||
* @var AttestationStatementSupport[]
|
||||
*/
|
||||
private array $attestationStatementSupports = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
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,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use Cose\Key\Ec2Key;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
use function openssl_pkey_get_public;
|
||||
use function openssl_verify;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
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'],
|
||||
new CertificateTrustPath($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$authenticatorData->getAttestedCredentialData()
|
||||
?->getAaguid()
|
||||
->__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->getTrustPath();
|
||||
$trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create(
|
||||
$attestationStatement,
|
||||
'Invalid trust path'
|
||||
);
|
||||
$dataToVerify = "\0";
|
||||
$dataToVerify .= $authenticatorData->getRpIdHash();
|
||||
$dataToVerify .= $clientDataJSONHash;
|
||||
$dataToVerify .= $authenticatorData->getAttestedCredentialData()
|
||||
->getCredentialId();
|
||||
$dataToVerify .= $this->extractPublicKey(
|
||||
$authenticatorData->getAttestedCredentialData()
|
||||
->getCredentialPublicKey()
|
||||
);
|
||||
|
||||
return openssl_verify(
|
||||
$dataToVerify,
|
||||
$attestationStatement->get('sig'),
|
||||
$trustPath->getCertificates()[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 function count;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
|
||||
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->getAttStmt()) === 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use Cose\Algorithm\Manager;
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Key;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function openssl_verify;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
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\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
use Webauthn\Util\CoseSignatureFixer;
|
||||
|
||||
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->getTrustPath();
|
||||
|
||||
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'],
|
||||
new CertificateTrustPath($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'],
|
||||
new EmptyTrustPath()
|
||||
);
|
||||
$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->getAttestedCredentialData();
|
||||
$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->getAaguid()
|
||||
->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->getCertificates();
|
||||
|
||||
// Check leaf certificate
|
||||
$this->checkCertificate($certificates[0], $authenticatorData);
|
||||
|
||||
// Get the COSE algorithm identifier and the corresponding OpenSSL one
|
||||
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
|
||||
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
|
||||
|
||||
// Verification of the signature
|
||||
$signedData = $authenticatorData->getAuthData() . $clientDataJSONHash;
|
||||
$result = openssl_verify(
|
||||
$signedData,
|
||||
$attestationStatement->get('sig'),
|
||||
$certificates[0],
|
||||
$opensslAlgorithmIdentifier
|
||||
);
|
||||
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
private function processWithECDAA(): never
|
||||
{
|
||||
throw UnsupportedFeatureException::create('ECDAA not supported');
|
||||
}
|
||||
|
||||
private function processWithSelfAttestation(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
|
||||
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
|
||||
'No attested credential available'
|
||||
);
|
||||
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
|
||||
$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->getAuthData() . $clientDataJSONHash;
|
||||
$algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg'));
|
||||
if (! $algorithm instanceof Signature) {
|
||||
throw InvalidDataException::create($algorithm, 'Invalid algorithm');
|
||||
}
|
||||
$signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm);
|
||||
|
||||
return $algorithm->verify($dataToVerify, $publicKey, $signature);
|
||||
}
|
||||
}
|
||||
445
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php
vendored
Normal file
445
libraries/vendor/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php
vendored
Normal file
@ -0,0 +1,445 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AttestationStatement;
|
||||
|
||||
use function array_key_exists;
|
||||
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 function count;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use Lcobucci\Clock\Clock;
|
||||
use Lcobucci\Clock\SystemClock;
|
||||
use function openssl_verify;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Psr\Clock\ClockInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use function unpack;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\Event\AttestationStatementLoaded;
|
||||
use Webauthn\Exception\AttestationStatementLoadingException;
|
||||
use Webauthn\Exception\AttestationStatementVerificationException;
|
||||
use Webauthn\Exception\InvalidAttestationStatementException;
|
||||
use Webauthn\Exception\UnsupportedFeatureException;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
|
||||
|
||||
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'],
|
||||
new CertificateTrustPath($certificates)
|
||||
);
|
||||
$this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
|
||||
|
||||
return $attestationStatement;
|
||||
}
|
||||
|
||||
public function isValid(
|
||||
string $clientDataJSONHash,
|
||||
AttestationStatement $attestationStatement,
|
||||
AuthenticatorData $authenticatorData
|
||||
): bool {
|
||||
$attToBeSigned = $authenticatorData->getAuthData() . $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->getAttestedCredentialData()?->getCredentialPublicKey();
|
||||
$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->getTrustPath() instanceof CertificateTrustPath => $this->processWithCertificate(
|
||||
$attestationStatement,
|
||||
$authenticatorData
|
||||
),
|
||||
$attestationStatement->getTrustPath() 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->getTrustPath();
|
||||
$trustPath instanceof CertificateTrustPath || throw AttestationStatementVerificationException::create(
|
||||
'Invalid trust path'
|
||||
);
|
||||
|
||||
$certificates = $trustPath->getCertificates();
|
||||
|
||||
// Check certificate CA chain and returns the Attestation Certificate
|
||||
$this->checkCertificate($certificates[0], $authenticatorData);
|
||||
|
||||
// Get the COSE algorithm identifier and the corresponding OpenSSL one
|
||||
$coseAlgorithmIdentifier = (int) $attestationStatement->get('alg');
|
||||
$opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier);
|
||||
|
||||
$result = openssl_verify(
|
||||
$attestationStatement->get('certInfo'),
|
||||
$attestationStatement->get('sig'),
|
||||
$certificates[0],
|
||||
$opensslAlgorithmIdentifier
|
||||
);
|
||||
|
||||
return $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->getAttestedCredentialData()
|
||||
?->getAaguid()
|
||||
->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');
|
||||
}
|
||||
}
|
||||
101
libraries/vendor/web-auth/webauthn-lib/src/AttestedCredentialData.php
vendored
Normal file
101
libraries/vendor/web-auth/webauthn-lib/src/AttestedCredentialData.php
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_string;
|
||||
use JsonSerializable;
|
||||
use ParagonIE\ConstantTime\Base64;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#sec-attested-credential-data
|
||||
*/
|
||||
class AttestedCredentialData implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
private AbstractUid $aaguid,
|
||||
private readonly string $credentialId,
|
||||
private readonly ?string $credentialPublicKey
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAaguid(): AbstractUid
|
||||
{
|
||||
return $this->aaguid;
|
||||
}
|
||||
|
||||
public function setAaguid(AbstractUid $aaguid): void
|
||||
{
|
||||
$this->aaguid = $aaguid;
|
||||
}
|
||||
|
||||
public function getCredentialId(): string
|
||||
{
|
||||
return $this->credentialId;
|
||||
}
|
||||
|
||||
public function getCredentialPublicKey(): ?string
|
||||
{
|
||||
return $this->credentialPublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
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 new self($uuid, $credentialId, $credentialPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$result = [
|
||||
'aaguid' => $this->aaguid->__toString(),
|
||||
'credentialId' => base64_encode($this->credentialId),
|
||||
];
|
||||
if ($this->credentialPublicKey !== null) {
|
||||
$result['credentialPublicKey'] = base64_encode($this->credentialPublicKey);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
36
libraries/vendor/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php
vendored
Normal file
36
libraries/vendor/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class AuthenticationExtension implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly mixed $value
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(string $name, mixed $value): self
|
||||
{
|
||||
return new self($name, $value);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function value(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use function array_key_exists;
|
||||
use ArrayIterator;
|
||||
use function count;
|
||||
use const COUNT_NORMAL;
|
||||
use Countable;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\AuthenticationExtensionException;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<AuthenticationExtension>
|
||||
*/
|
||||
class AuthenticationExtensionsClientInputs implements JsonSerializable, Countable, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var AuthenticationExtension[]
|
||||
*/
|
||||
private array $extensions = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function add(AuthenticationExtension ...$extensions): self
|
||||
{
|
||||
foreach ($extensions as $extension) {
|
||||
$this->extensions[$extension->name()] = $extension;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
$object = new self();
|
||||
foreach ($json as $k => $v) {
|
||||
$object->add(AuthenticationExtension::create($k, $v));
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
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 mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (AuthenticationExtension $object): mixed => $object->jsonSerialize(),
|
||||
$this->extensions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Iterator<string, AuthenticationExtension>
|
||||
*/
|
||||
public function getIterator(): Iterator
|
||||
{
|
||||
return new ArrayIterator($this->extensions);
|
||||
}
|
||||
|
||||
public function count(int $mode = COUNT_NORMAL): int
|
||||
{
|
||||
return count($this->extensions, $mode);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use function array_key_exists;
|
||||
use ArrayIterator;
|
||||
use function count;
|
||||
use const COUNT_NORMAL;
|
||||
use Countable;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\AuthenticationExtensionException;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<AuthenticationExtension>
|
||||
*/
|
||||
class AuthenticationExtensionsClientOutputs implements JsonSerializable, Countable, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var AuthenticationExtension[]
|
||||
*/
|
||||
private array $extensions = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function add(AuthenticationExtension ...$extensions): void
|
||||
{
|
||||
foreach ($extensions as $extension) {
|
||||
$this->extensions[$extension->name()] = $extension;
|
||||
}
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
$object = new self();
|
||||
foreach ($json as $k => $v) {
|
||||
$object->add(AuthenticationExtension::create($k, $v));
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
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 mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (AuthenticationExtension $object): mixed => $object->jsonSerialize(),
|
||||
$this->extensions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Iterator<string, AuthenticationExtension>
|
||||
*/
|
||||
public function getIterator(): Iterator
|
||||
{
|
||||
return new ArrayIterator($this->extensions);
|
||||
}
|
||||
|
||||
public function count(int $mode = COUNT_NORMAL): int
|
||||
{
|
||||
return count($this->extensions, $mode);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use CBOR\CBORObject;
|
||||
use CBOR\MapObject;
|
||||
use function is_string;
|
||||
use Webauthn\Exception\AuthenticationExtensionException;
|
||||
|
||||
abstract class AuthenticationExtensionsClientOutputsLoader
|
||||
{
|
||||
public static function load(CBORObject $object): AuthenticationExtensionsClientOutputs
|
||||
{
|
||||
$object instanceof MapObject || throw AuthenticationExtensionException::create('Invalid extension object');
|
||||
$data = $object->normalize();
|
||||
$extensions = AuthenticationExtensionsClientOutputs::create();
|
||||
foreach ($data as $key => $value) {
|
||||
is_string($key) || throw AuthenticationExtensionException::create('Invalid extension key');
|
||||
$extensions->add(AuthenticationExtension::create($key, $value));
|
||||
}
|
||||
|
||||
return $extensions;
|
||||
}
|
||||
}
|
||||
13
libraries/vendor/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php
vendored
Normal file
13
libraries/vendor/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
interface ExtensionOutputChecker
|
||||
{
|
||||
public function check(
|
||||
AuthenticationExtensionsClientInputs $inputs,
|
||||
AuthenticationExtensionsClientOutputs $outputs
|
||||
): void;
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
<?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(
|
||||
AuthenticationExtensionsClientInputs $inputs,
|
||||
AuthenticationExtensionsClientOutputs $outputs
|
||||
): void {
|
||||
foreach ($this->checkers as $checker) {
|
||||
$checker->check($inputs, $outputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
libraries/vendor/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php
vendored
Normal file
25
libraries/vendor/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\AuthenticationExtensions;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class ExtensionOutputError extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthenticationExtension $authenticationExtension,
|
||||
string $message = '',
|
||||
int $code = 0,
|
||||
Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getAuthenticationExtension(): AuthenticationExtension
|
||||
{
|
||||
return $this->authenticationExtension;
|
||||
}
|
||||
}
|
||||
41
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php
vendored
Normal file
41
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Webauthn\Util\Base64;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#authenticatorassertionresponse
|
||||
*/
|
||||
class AuthenticatorAssertionResponse extends AuthenticatorResponse
|
||||
{
|
||||
public function __construct(
|
||||
CollectedClientData $clientDataJSON,
|
||||
private readonly AuthenticatorData $authenticatorData,
|
||||
private readonly string $signature,
|
||||
private readonly ?string $userHandle
|
||||
) {
|
||||
parent::__construct($clientDataJSON);
|
||||
}
|
||||
|
||||
public function getAuthenticatorData(): AuthenticatorData
|
||||
{
|
||||
return $this->authenticatorData;
|
||||
}
|
||||
|
||||
public function getSignature(): string
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
public function getUserHandle(): ?string
|
||||
{
|
||||
if ($this->userHandle === null || $this->userHandle === '') {
|
||||
return $this->userHandle;
|
||||
}
|
||||
|
||||
return Base64::decode($this->userHandle);
|
||||
}
|
||||
}
|
||||
404
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php
vendored
Normal file
404
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php
vendored
Normal file
@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use CBOR\Decoder;
|
||||
use CBOR\Normalizable;
|
||||
use Cose\Algorithm\Manager;
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Key\Key;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function parse_url;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\Counter\CounterChecker;
|
||||
use Webauthn\Counter\ThrowExceptionIfInvalid;
|
||||
use Webauthn\Event\AuthenticatorAssertionResponseValidationFailedEvent;
|
||||
use Webauthn\Event\AuthenticatorAssertionResponseValidationSucceededEvent;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\TokenBinding\TokenBindingHandler;
|
||||
use Webauthn\Util\CoseSignatureFixer;
|
||||
|
||||
class AuthenticatorAssertionResponseValidator implements CanLogData, CanDispatchEvents
|
||||
{
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private CounterChecker $counterChecker;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
private readonly PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository,
|
||||
private readonly ?TokenBindingHandler $tokenBindingHandler,
|
||||
private readonly ExtensionOutputCheckerHandler $extensionOutputCheckerHandler,
|
||||
private readonly ?Manager $algorithmManager,
|
||||
?EventDispatcherInterface $eventDispatcher = null,
|
||||
) {
|
||||
if ($this->tokenBindingHandler !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-symfony-bundle',
|
||||
'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 ($eventDispatcher === null) {
|
||||
$this->eventDispatcher = new NullEventDispatcher();
|
||||
} else {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
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.'
|
||||
);
|
||||
}
|
||||
$this->decoder = Decoder::create();
|
||||
$this->counterChecker = new ThrowExceptionIfInvalid();
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public static function create(
|
||||
PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository,
|
||||
?TokenBindingHandler $tokenBindingHandler,
|
||||
ExtensionOutputCheckerHandler $extensionOutputCheckerHandler,
|
||||
?Manager $algorithmManager,
|
||||
?EventDispatcherInterface $eventDispatcher = null
|
||||
): self {
|
||||
return new self(
|
||||
$publicKeyCredentialSourceRepository,
|
||||
$tokenBindingHandler,
|
||||
$extensionOutputCheckerHandler,
|
||||
$algorithmManager,
|
||||
$eventDispatcher,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $securedRelyingPartyId
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||
*/
|
||||
public function check(
|
||||
string $credentialId,
|
||||
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
?string $userHandle,
|
||||
array $securedRelyingPartyId = []
|
||||
): 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
|
||||
)
|
||||
);
|
||||
}
|
||||
try {
|
||||
$this->logger->info('Checking the authenticator assertion response', [
|
||||
'credentialId' => $credentialId,
|
||||
'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
|
||||
'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
|
||||
'host' => is_string($request) ? $request : $request->getUri()
|
||||
->getHost(),
|
||||
'userHandle' => $userHandle,
|
||||
]);
|
||||
if (count($publicKeyCredentialRequestOptions->getAllowCredentials()) !== 0) {
|
||||
$this->isCredentialIdAllowed(
|
||||
$credentialId,
|
||||
$publicKeyCredentialRequestOptions->getAllowCredentials()
|
||||
) || throw AuthenticatorResponseVerificationException::create('The credential ID is not allowed.');
|
||||
}
|
||||
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId(
|
||||
$credentialId
|
||||
);
|
||||
$publicKeyCredentialSource !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'The credential ID is invalid.'
|
||||
);
|
||||
$attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
|
||||
$credentialUserHandle = $publicKeyCredentialSource->getUserHandle();
|
||||
$responseUserHandle = $authenticatorAssertionResponse->getUserHandle();
|
||||
if ($userHandle !== null) { //If the user was identified before the authentication ceremony was initiated,
|
||||
$credentialUserHandle === $userHandle || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid user handle'
|
||||
);
|
||||
if ($responseUserHandle !== null && $responseUserHandle !== '') {
|
||||
$credentialUserHandle === $responseUserHandle || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid user handle'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
($responseUserHandle !== '' && $credentialUserHandle === $responseUserHandle) || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid user handle'
|
||||
);
|
||||
}
|
||||
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
|
||||
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'No public key available.'
|
||||
);
|
||||
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
|
||||
if ($isU2F === true) {
|
||||
$credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey);
|
||||
}
|
||||
$stream = new StringStream($credentialPublicKey);
|
||||
$credentialPublicKeyStream = $this->decoder->decode($stream);
|
||||
$stream->isEOF() || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid key. Presence of extra bytes.'
|
||||
);
|
||||
$stream->close();
|
||||
$C = $authenticatorAssertionResponse->getClientDataJSON();
|
||||
$C->getType() === 'webauthn.get' || throw AuthenticatorResponseVerificationException::create(
|
||||
'The client data type is not "webauthn.get".'
|
||||
);
|
||||
hash_equals(
|
||||
$publicKeyCredentialRequestOptions->getChallenge(),
|
||||
$C->getChallenge()
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.');
|
||||
$rpId = $publicKeyCredentialRequestOptions->getRpId() ?? (is_string(
|
||||
$request
|
||||
) ? $request : $request->getUri()
|
||||
->getHost());
|
||||
$facetId = $this->getFacetId(
|
||||
$rpId,
|
||||
$publicKeyCredentialRequestOptions->getExtensions(),
|
||||
$authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->getExtensions()
|
||||
);
|
||||
$parsedRelyingPartyId = parse_url($C->getOrigin());
|
||||
is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid origin'
|
||||
);
|
||||
if (! in_array($facetId, $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.');
|
||||
if (! is_string($request) && $C->getTokenBinding() !== null) {
|
||||
$this->tokenBindingHandler?->check($C->getTokenBinding(), $request);
|
||||
}
|
||||
$rpIdHash = hash('sha256', $isU2F ? $C->getOrigin() : $facetId, true);
|
||||
hash_equals(
|
||||
$rpIdHash,
|
||||
$authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->getRpIdHash()
|
||||
) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.');
|
||||
if ($publicKeyCredentialRequestOptions->getUserVerification() === AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) {
|
||||
$authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->isUserPresent() || throw AuthenticatorResponseVerificationException::create(
|
||||
'User was not present'
|
||||
);
|
||||
$authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->isUserVerified() || throw AuthenticatorResponseVerificationException::create(
|
||||
'User authentication required.'
|
||||
);
|
||||
}
|
||||
$extensionsClientOutputs = $authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->getExtensions();
|
||||
if ($extensionsClientOutputs !== null) {
|
||||
$this->extensionOutputCheckerHandler->check(
|
||||
$publicKeyCredentialRequestOptions->getExtensions(),
|
||||
$extensionsClientOutputs
|
||||
);
|
||||
}
|
||||
$getClientDataJSONHash = hash(
|
||||
'sha256',
|
||||
$authenticatorAssertionResponse->getClientDataJSON()
|
||||
->getRawData(),
|
||||
true
|
||||
);
|
||||
$dataToVerify = $authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->getAuthData() . $getClientDataJSONHash;
|
||||
$signature = $authenticatorAssertionResponse->getSignature();
|
||||
$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.'
|
||||
);
|
||||
$coseKey = Key::create($normalizedData);
|
||||
$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.');
|
||||
$storedCounter = $publicKeyCredentialSource->getCounter();
|
||||
$responseCounter = $authenticatorAssertionResponse->getAuthenticatorData()
|
||||
->getSignCount();
|
||||
if ($responseCounter !== 0 || $storedCounter !== 0) {
|
||||
$this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
|
||||
}
|
||||
$publicKeyCredentialSource->setCounter($responseCounter);
|
||||
$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(
|
||||
$credentialId,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$request,
|
||||
$userHandle,
|
||||
$publicKeyCredentialSource
|
||||
)
|
||||
);
|
||||
return $publicKeyCredentialSource;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAssertionResponseValidationFailedEvent(
|
||||
$credentialId,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$request,
|
||||
$userHandle,
|
||||
$throwable
|
||||
)
|
||||
);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function setCounterChecker(CounterChecker $counterChecker): self
|
||||
{
|
||||
$this->counterChecker = $counterChecker;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAssertionResponseValidationSucceededEvent(
|
||||
string $credentialId,
|
||||
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
?string $userHandle,
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource
|
||||
): AuthenticatorAssertionResponseValidationSucceededEvent {
|
||||
if ($request 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,
|
||||
$request,
|
||||
$userHandle,
|
||||
$publicKeyCredentialSource
|
||||
);
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAssertionResponseValidationFailedEvent(
|
||||
string $credentialId,
|
||||
AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
?string $userHandle,
|
||||
Throwable $throwable
|
||||
): AuthenticatorAssertionResponseValidationFailedEvent {
|
||||
if ($request 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(
|
||||
$credentialId,
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$request,
|
||||
$userHandle,
|
||||
$throwable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<PublicKeyCredentialDescriptor> $allowedCredentials
|
||||
*/
|
||||
private function isCredentialIdAllowed(string $credentialId, array $allowedCredentials): bool
|
||||
{
|
||||
foreach ($allowedCredentials as $allowedCredential) {
|
||||
if (hash_equals($allowedCredential->getId(), $credentialId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getFacetId(
|
||||
string $rpId,
|
||||
AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs,
|
||||
?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs
|
||||
): string {
|
||||
if ($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;
|
||||
}
|
||||
}
|
||||
25
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php
vendored
Normal file
25
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationObject;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#authenticatorattestationresponse
|
||||
*/
|
||||
class AuthenticatorAttestationResponse extends AuthenticatorResponse
|
||||
{
|
||||
public function __construct(
|
||||
CollectedClientData $clientDataJSON,
|
||||
private readonly AttestationObject $attestationObject
|
||||
) {
|
||||
parent::__construct($clientDataJSON);
|
||||
}
|
||||
|
||||
public function getAttestationObject(): AttestationObject
|
||||
{
|
||||
return $this->attestationObject;
|
||||
}
|
||||
}
|
||||
510
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php
vendored
Normal file
510
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php
vendored
Normal file
@ -0,0 +1,510 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function parse_url;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Throwable;
|
||||
use Webauthn\AttestationStatement\AttestationObject;
|
||||
use Webauthn\AttestationStatement\AttestationStatement;
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\Event\AuthenticatorAttestationResponseValidationFailedEvent;
|
||||
use Webauthn\Event\AuthenticatorAttestationResponseValidationSucceededEvent;
|
||||
use Webauthn\Exception\AuthenticatorResponseVerificationException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateToolbox;
|
||||
use Webauthn\MetadataService\Event\CanDispatchEvents;
|
||||
use Webauthn\MetadataService\Event\NullEventDispatcher;
|
||||
use Webauthn\MetadataService\MetadataStatementRepository;
|
||||
use Webauthn\MetadataService\Statement\MetadataStatement;
|
||||
use Webauthn\MetadataService\StatusReportRepository;
|
||||
use Webauthn\TokenBinding\TokenBindingHandler;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
|
||||
class AuthenticatorAttestationResponseValidator implements CanLogData, CanDispatchEvents
|
||||
{
|
||||
private LoggerInterface $logger;
|
||||
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
private ?MetadataStatementRepository $metadataStatementRepository = null;
|
||||
|
||||
private ?StatusReportRepository $statusReportRepository = null;
|
||||
|
||||
private ?CertificateChainValidator $certificateChainValidator = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly AttestationStatementSupportManager $attestationStatementSupportManager,
|
||||
private readonly PublicKeyCredentialSourceRepository $publicKeyCredentialSource,
|
||||
private readonly ?TokenBindingHandler $tokenBindingHandler,
|
||||
private readonly ExtensionOutputCheckerHandler $extensionOutputCheckerHandler,
|
||||
?EventDispatcherInterface $eventDispatcher = null,
|
||||
) {
|
||||
if ($this->tokenBindingHandler !== null) {
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-symfony-bundle',
|
||||
'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 ($eventDispatcher === null) {
|
||||
$this->eventDispatcher = new NullEventDispatcher();
|
||||
} else {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
trigger_deprecation(
|
||||
'web-auth/webauthn-symfony-bundle',
|
||||
'4.5.0',
|
||||
'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.'
|
||||
);
|
||||
}
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public static function create(
|
||||
AttestationStatementSupportManager $attestationStatementSupportManager,
|
||||
PublicKeyCredentialSourceRepository $publicKeyCredentialSource,
|
||||
?TokenBindingHandler $tokenBindingHandler,
|
||||
ExtensionOutputCheckerHandler $extensionOutputCheckerHandler,
|
||||
?EventDispatcherInterface $eventDispatcher = null
|
||||
): self {
|
||||
return new self(
|
||||
$attestationStatementSupportManager,
|
||||
$publicKeyCredentialSource,
|
||||
$tokenBindingHandler,
|
||||
$extensionOutputCheckerHandler,
|
||||
$eventDispatcher,
|
||||
);
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function setCertificateChainValidator(): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function enableMetadataStatementSupport(
|
||||
MetadataStatementRepository $metadataStatementRepository,
|
||||
StatusReportRepository $statusReportRepository,
|
||||
CertificateChainValidator $certificateChainValidator
|
||||
): self {
|
||||
$this->metadataStatementRepository = $metadataStatementRepository;
|
||||
$this->certificateChainValidator = $certificateChainValidator;
|
||||
$this->statusReportRepository = $statusReportRepository;
|
||||
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,
|
||||
array $securedRelyingPartyId = []
|
||||
): 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
|
||||
)
|
||||
);
|
||||
}
|
||||
try {
|
||||
$this->logger->info('Checking the authenticator attestation response', [
|
||||
'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
|
||||
'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
|
||||
'host' => is_string($request) ? $request : $request->getUri()
|
||||
->getHost(),
|
||||
]);
|
||||
//Nothing to do
|
||||
$C = $authenticatorAttestationResponse->getClientDataJSON();
|
||||
$C->getType() === 'webauthn.create' || throw AuthenticatorResponseVerificationException::create(
|
||||
'The client data type is not "webauthn.create".'
|
||||
);
|
||||
hash_equals(
|
||||
$publicKeyCredentialCreationOptions->getChallenge(),
|
||||
$C->getChallenge()
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.');
|
||||
$rpId = $publicKeyCredentialCreationOptions->getRp()
|
||||
->getId() ?? (is_string($request) ? $request : $request->getUri()->getHost());
|
||||
$facetId = $this->getFacetId(
|
||||
$rpId,
|
||||
$publicKeyCredentialCreationOptions->getExtensions(),
|
||||
$authenticatorAttestationResponse->getAttestationObject()
|
||||
->getAuthData()
|
||||
->getExtensions()
|
||||
);
|
||||
$parsedRelyingPartyId = parse_url($C->getOrigin());
|
||||
is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create(
|
||||
sprintf('The origin URI "%s" is not valid', $C->getOrigin())
|
||||
);
|
||||
array_key_exists(
|
||||
'scheme',
|
||||
$parsedRelyingPartyId
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.');
|
||||
$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.');
|
||||
if (! in_array($facetId, $securedRelyingPartyId, true)) {
|
||||
$scheme = $parsedRelyingPartyId['scheme'];
|
||||
$scheme === 'https' || throw AuthenticatorResponseVerificationException::create(
|
||||
'Invalid scheme. HTTPS required.'
|
||||
);
|
||||
}
|
||||
if (! is_string($request) && $C->getTokenBinding() !== null) {
|
||||
$this->tokenBindingHandler?->check($C->getTokenBinding(), $request);
|
||||
}
|
||||
$clientDataJSONHash = hash(
|
||||
'sha256',
|
||||
$authenticatorAttestationResponse->getClientDataJSON()
|
||||
->getRawData(),
|
||||
true
|
||||
);
|
||||
$attestationObject = $authenticatorAttestationResponse->getAttestationObject();
|
||||
$rpIdHash = hash('sha256', $facetId, true);
|
||||
hash_equals(
|
||||
$rpIdHash,
|
||||
$attestationObject->getAuthData()
|
||||
->getRpIdHash()
|
||||
) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.');
|
||||
if ($publicKeyCredentialCreationOptions->getAuthenticatorSelection()?->getUserVerification() === AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) {
|
||||
$attestationObject->getAuthData()
|
||||
->isUserPresent() || throw AuthenticatorResponseVerificationException::create(
|
||||
'User was not present'
|
||||
);
|
||||
$attestationObject->getAuthData()
|
||||
->isUserVerified() || throw AuthenticatorResponseVerificationException::create(
|
||||
'User authentication required.'
|
||||
);
|
||||
}
|
||||
$extensionsClientOutputs = $attestationObject->getAuthData()
|
||||
->getExtensions();
|
||||
if ($extensionsClientOutputs !== null) {
|
||||
$this->extensionOutputCheckerHandler->check(
|
||||
$publicKeyCredentialCreationOptions->getExtensions(),
|
||||
$extensionsClientOutputs
|
||||
);
|
||||
}
|
||||
$this->checkMetadataStatement($publicKeyCredentialCreationOptions, $attestationObject);
|
||||
$fmt = $attestationObject->getAttStmt()
|
||||
->getFmt();
|
||||
$this->attestationStatementSupportManager->has(
|
||||
$fmt
|
||||
) || throw AuthenticatorResponseVerificationException::create(
|
||||
'Unsupported attestation statement format.'
|
||||
);
|
||||
$attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
|
||||
$attestationStatementSupport->isValid(
|
||||
$clientDataJSONHash,
|
||||
$attestationObject->getAttStmt(),
|
||||
$attestationObject->getAuthData()
|
||||
) || throw AuthenticatorResponseVerificationException::create('Invalid attestation statement.');
|
||||
$attestationObject->getAuthData()
|
||||
->hasAttestedCredentialData() || throw AuthenticatorResponseVerificationException::create(
|
||||
'There is no attested credential data.'
|
||||
);
|
||||
$attestedCredentialData = $attestationObject->getAuthData()
|
||||
->getAttestedCredentialData();
|
||||
$attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'There is no attested credential data.'
|
||||
);
|
||||
$credentialId = $attestedCredentialData->getCredentialId();
|
||||
$this->publicKeyCredentialSource->findOneByCredentialId(
|
||||
$credentialId
|
||||
) === null || throw AuthenticatorResponseVerificationException::create(
|
||||
'The credential ID already exists.'
|
||||
);
|
||||
$publicKeyCredentialSource = $this->createPublicKeyCredentialSource(
|
||||
$credentialId,
|
||||
$attestedCredentialData,
|
||||
$attestationObject,
|
||||
$publicKeyCredentialCreationOptions->getUser()
|
||||
->getId()
|
||||
);
|
||||
$this->logger->info('The attestation is valid');
|
||||
$this->logger->debug('Public Key Credential Source', [
|
||||
'publicKeyCredentialSource' => $publicKeyCredentialSource,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAttestationResponseValidationSucceededEvent(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$request,
|
||||
$publicKeyCredentialSource
|
||||
)
|
||||
);
|
||||
return $publicKeyCredentialSource;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
$this->createAuthenticatorAttestationResponseValidationFailedEvent(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$request,
|
||||
$throwable
|
||||
)
|
||||
);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAttestationResponseValidationSucceededEvent(
|
||||
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
PublicKeyCredentialSource $publicKeyCredentialSource
|
||||
): AuthenticatorAttestationResponseValidationSucceededEvent {
|
||||
if ($request 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,
|
||||
$request,
|
||||
$publicKeyCredentialSource
|
||||
);
|
||||
}
|
||||
|
||||
protected function createAuthenticatorAttestationResponseValidationFailedEvent(
|
||||
AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
ServerRequestInterface|string $request,
|
||||
Throwable $throwable
|
||||
): AuthenticatorAttestationResponseValidationFailedEvent {
|
||||
if ($request 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,
|
||||
$request,
|
||||
$throwable
|
||||
);
|
||||
}
|
||||
|
||||
private function checkCertificateChain(
|
||||
AttestationStatement $attestationStatement,
|
||||
?MetadataStatement $metadataStatement
|
||||
): void {
|
||||
$trustPath = $attestationStatement->getTrustPath();
|
||||
if (! $trustPath instanceof CertificateTrustPath) {
|
||||
return;
|
||||
}
|
||||
$authenticatorCertificates = $trustPath->getCertificates();
|
||||
if ($metadataStatement === null) {
|
||||
$this->certificateChainValidator?->check($authenticatorCertificates, []);
|
||||
return;
|
||||
}
|
||||
$trustedCertificates = CertificateToolbox::fixPEMStructures(
|
||||
$metadataStatement->getAttestationRootCertificates()
|
||||
);
|
||||
$this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates);
|
||||
}
|
||||
|
||||
private function checkMetadataStatement(
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
AttestationObject $attestationObject
|
||||
): void {
|
||||
$attestationStatement = $attestationObject->getAttStmt();
|
||||
$attestedCredentialData = $attestationObject->getAuthData()
|
||||
->getAttestedCredentialData();
|
||||
$attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'No attested credential data found'
|
||||
);
|
||||
$aaguid = $attestedCredentialData->getAaguid()
|
||||
->__toString();
|
||||
if ($publicKeyCredentialCreationOptions->getAttestation() === null || $publicKeyCredentialCreationOptions->getAttestation() === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) {
|
||||
$this->logger->debug('No attestation is asked.');
|
||||
//No attestation is asked. We shall ensure that the data is anonymous.
|
||||
if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array(
|
||||
$attestationStatement->getType(),
|
||||
[AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF],
|
||||
true
|
||||
)) {
|
||||
$this->logger->debug('The Attestation Statement is anonymous.');
|
||||
$this->checkCertificateChain($attestationStatement, null);
|
||||
return;
|
||||
}
|
||||
$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
|
||||
'aaguid' => $aaguid,
|
||||
'AttestationStatement' => $attestationStatement,
|
||||
]);
|
||||
$attestedCredentialData->setAaguid(Uuid::fromString('00000000-0000-0000-0000-000000000000'));
|
||||
$attestationObject->setAttStmt(AttestationStatement::createNone('none', [], new EmptyTrustPath()));
|
||||
return;
|
||||
}
|
||||
// If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
|
||||
// => nothing to check
|
||||
if ($attestationStatement->getType() === 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->setAaguid(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->getAttestationTypes()) !== 0) {
|
||||
$type = $this->getAttestationType($attestationStatement);
|
||||
in_array(
|
||||
$type,
|
||||
$metadataStatement->getAttestationTypes(),
|
||||
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->getType()) {
|
||||
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 createPublicKeyCredentialSource(
|
||||
string $credentialId,
|
||||
AttestedCredentialData $attestedCredentialData,
|
||||
AttestationObject $attestationObject,
|
||||
string $userHandle
|
||||
): PublicKeyCredentialSource {
|
||||
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
|
||||
$credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create(
|
||||
'Not credential public key available in the attested credential data'
|
||||
);
|
||||
return new PublicKeyCredentialSource(
|
||||
$credentialId,
|
||||
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
|
||||
[],
|
||||
$attestationObject->getAttStmt()
|
||||
->getType(),
|
||||
$attestationObject->getAttStmt()
|
||||
->getTrustPath(),
|
||||
$attestedCredentialData->getAaguid(),
|
||||
$credentialPublicKey,
|
||||
$userHandle,
|
||||
$attestationObject->getAuthData()
|
||||
->getSignCount()
|
||||
);
|
||||
}
|
||||
|
||||
private function getFacetId(
|
||||
string $rpId,
|
||||
AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs,
|
||||
?AuthenticationExtensionsClientOutputs $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;
|
||||
}
|
||||
}
|
||||
91
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorData.php
vendored
Normal file
91
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorData.php
vendored
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function ord;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#sec-authenticator-data
|
||||
*/
|
||||
class AuthenticatorData
|
||||
{
|
||||
private const FLAG_UP = 0b00000001;
|
||||
|
||||
private const FLAG_RFU1 = 0b00000010;
|
||||
|
||||
private const FLAG_UV = 0b00000100;
|
||||
|
||||
private const FLAG_RFU2 = 0b00111000;
|
||||
|
||||
private const FLAG_AT = 0b01000000;
|
||||
|
||||
private const FLAG_ED = 0b10000000;
|
||||
|
||||
public function __construct(
|
||||
protected string $authData,
|
||||
protected string $rpIdHash,
|
||||
protected string $flags,
|
||||
protected int $signCount,
|
||||
protected ?AttestedCredentialData $attestedCredentialData,
|
||||
protected ?AuthenticationExtensionsClientOutputs $extensions
|
||||
) {
|
||||
}
|
||||
|
||||
public function getAuthData(): string
|
||||
{
|
||||
return $this->authData;
|
||||
}
|
||||
|
||||
public function getRpIdHash(): string
|
||||
{
|
||||
return $this->rpIdHash;
|
||||
}
|
||||
|
||||
public function isUserPresent(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_UP);
|
||||
}
|
||||
|
||||
public function isUserVerified(): bool
|
||||
{
|
||||
return 0 !== (ord($this->flags) & self::FLAG_UV);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getSignCount(): int
|
||||
{
|
||||
return $this->signCount;
|
||||
}
|
||||
|
||||
public function getAttestedCredentialData(): ?AttestedCredentialData
|
||||
{
|
||||
return $this->attestedCredentialData;
|
||||
}
|
||||
|
||||
public function getExtensions(): ?AuthenticationExtensionsClientOutputs
|
||||
{
|
||||
return $this->extensions !== null && $this->hasExtensions() ? $this->extensions : null;
|
||||
}
|
||||
}
|
||||
21
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorResponse.php
vendored
Normal file
21
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorResponse.php
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#authenticatorresponse
|
||||
*/
|
||||
abstract class AuthenticatorResponse
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectedClientData $clientDataJSON
|
||||
) {
|
||||
}
|
||||
|
||||
public function getClientDataJSON(): CollectedClientData
|
||||
{
|
||||
return $this->clientDataJSON;
|
||||
}
|
||||
}
|
||||
163
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php
vendored
Normal file
163
libraries/vendor/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function is_bool;
|
||||
use function is_string;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
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 USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
|
||||
|
||||
final public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
|
||||
|
||||
final public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
|
||||
|
||||
final public const RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE = null;
|
||||
|
||||
/**
|
||||
* @deprecated Please use AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE instead
|
||||
*/
|
||||
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';
|
||||
|
||||
private ?string $authenticatorAttachment = null;
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in 5.0. Please use residentKey instead
|
||||
*/
|
||||
private bool $requireResidentKey = false;
|
||||
|
||||
private string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED;
|
||||
|
||||
private null|string $residentKey = self::RESIDENT_KEY_REQUIREMENT_PREFERRED;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function setAuthenticatorAttachment(?string $authenticatorAttachment): self
|
||||
{
|
||||
$this->authenticatorAttachment = $authenticatorAttachment;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use setResidentKey instead
|
||||
*/
|
||||
public function setRequireResidentKey(bool $requireResidentKey): self
|
||||
{
|
||||
$this->requireResidentKey = $requireResidentKey;
|
||||
//$this->residentKey = $requireResidentKey ? self::RESIDENT_KEY_REQUIREMENT_REQUIRED : self::RESIDENT_KEY_REQUIREMENT_DISCOURAGED;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUserVerification(string $userVerification): self
|
||||
{
|
||||
$this->userVerification = $userVerification;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setResidentKey(null|string $residentKey): self
|
||||
{
|
||||
$this->residentKey = $residentKey;
|
||||
//$this->requireResidentKey = $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuthenticatorAttachment(): ?string
|
||||
{
|
||||
return $this->authenticatorAttachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Will be removed in 5.0. Please use getResidentKey() instead
|
||||
*/
|
||||
public function isRequireResidentKey(): bool
|
||||
{
|
||||
return $this->requireResidentKey;
|
||||
}
|
||||
|
||||
public function getUserVerification(): string
|
||||
{
|
||||
return $this->userVerification;
|
||||
}
|
||||
|
||||
public function getResidentKey(): null|string
|
||||
{
|
||||
return $this->residentKey;
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
$authenticatorAttachment = $json['authenticatorAttachment'] ?? null;
|
||||
$requireResidentKey = $json['requireResidentKey'] ?? false;
|
||||
$userVerification = $json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED;
|
||||
$residentKey = $json['residentKey'] ?? self::RESIDENT_KEY_REQUIREMENT_PREFERRED;
|
||||
|
||||
$authenticatorAttachment === null || is_string($authenticatorAttachment) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid "authenticatorAttachment" value'
|
||||
);
|
||||
is_bool($requireResidentKey) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid "requireResidentKey" value'
|
||||
);
|
||||
is_string($userVerification) || throw InvalidDataException::create($json, 'Invalid "userVerification" value');
|
||||
is_string($residentKey) || throw InvalidDataException::create($json, 'Invalid "residentKey" value');
|
||||
|
||||
return self::create()
|
||||
->setAuthenticatorAttachment($authenticatorAttachment)
|
||||
->setRequireResidentKey($requireResidentKey)
|
||||
->setUserVerification($userVerification)
|
||||
->setResidentKey($residentKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = [
|
||||
'requireResidentKey' => $this->requireResidentKey,
|
||||
'userVerification' => $this->userVerification,
|
||||
// 'residentKey' => $this->residentKey, // TODO: On hold. Waiting for issue clarification. See https://github.com/fido-alliance/conformance-test-tools-resources/issues/676
|
||||
];
|
||||
if ($this->authenticatorAttachment !== null) {
|
||||
$json['authenticatorAttachment'] = $this->authenticatorAttachment;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
14
libraries/vendor/web-auth/webauthn-lib/src/CertificateChainChecker/CertificateChainChecker.php
vendored
Normal file
14
libraries/vendor/web-auth/webauthn-lib/src/CertificateChainChecker/CertificateChainChecker.php
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CertificateChainChecker;
|
||||
|
||||
use Webauthn\MetadataService\CertificateChain\CertificateChainValidator;
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\CertificateChainValidator instead
|
||||
*/
|
||||
interface CertificateChainChecker extends CertificateChainValidator
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\CertificateChainChecker;
|
||||
|
||||
use Webauthn\MetadataService\CertificateChain\PhpCertificateChainValidator;
|
||||
|
||||
/**
|
||||
* @deprecated since v4.1. Please use Webauthn\MetadataService\CertificateChainChecker\PhpCertificateChainValidator instead
|
||||
*/
|
||||
final class PhpCertificateChainChecker extends PhpCertificateChainValidator
|
||||
{
|
||||
}
|
||||
14
libraries/vendor/web-auth/webauthn-lib/src/CertificateToolbox.php
vendored
Normal file
14
libraries/vendor/web-auth/webauthn-lib/src/CertificateToolbox.php
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<?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
|
||||
*/
|
||||
class CertificateToolbox extends BaseCertificateToolbox
|
||||
{
|
||||
}
|
||||
143
libraries/vendor/web-auth/webauthn-lib/src/CollectedClientData.php
vendored
Normal file
143
libraries/vendor/web-auth/webauthn-lib/src/CollectedClientData.php
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\TokenBinding\TokenBinding;
|
||||
|
||||
class CollectedClientData
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private readonly array $data;
|
||||
|
||||
private readonly string $type;
|
||||
|
||||
private readonly string $challenge;
|
||||
|
||||
private readonly string $origin;
|
||||
|
||||
private readonly bool $crossOrigin;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
private readonly ?array $tokenBinding;
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public function __construct(
|
||||
private 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->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;
|
||||
}
|
||||
|
||||
public static function createFormJson(string $data): self
|
||||
{
|
||||
$rawData = Base64UrlSafe::decodeNoPadding($data);
|
||||
$json = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return new self($rawData, $json);
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getChallenge(): string
|
||||
{
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
public function getOrigin(): string
|
||||
{
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
public function getCrossOrigin(): bool
|
||||
{
|
||||
return $this->crossOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
public function getTokenBinding(): ?TokenBinding
|
||||
{
|
||||
return $this->tokenBinding === null ? null : TokenBinding::createFormArray($this->tokenBinding);
|
||||
}
|
||||
|
||||
public function getRawData(): string
|
||||
{
|
||||
return $this->rawData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return array_keys($this->data);
|
||||
}
|
||||
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->data);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
12
libraries/vendor/web-auth/webauthn-lib/src/Counter/CounterChecker.php
vendored
Normal file
12
libraries/vendor/web-auth/webauthn-lib/src/Counter/CounterChecker.php
vendored
Normal file
@ -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;
|
||||
}
|
||||
42
libraries/vendor/web-auth/webauthn-lib/src/Counter/ThrowExceptionIfInvalid.php
vendored
Normal file
42
libraries/vendor/web-auth/webauthn-lib/src/Counter/ThrowExceptionIfInvalid.php
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Counter;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Throwable;
|
||||
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->getCounter() || throw CounterException::create(
|
||||
$currentCounter,
|
||||
$publicKeyCredentialSource->getCounter(),
|
||||
'Invalid counter.'
|
||||
);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('The counter is invalid', [
|
||||
'current' => $currentCounter,
|
||||
'new' => $publicKeyCredentialSource->getCounter(),
|
||||
]);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
libraries/vendor/web-auth/webauthn-lib/src/Credential.php
vendored
Normal file
27
libraries/vendor/web-auth/webauthn-lib/src/Credential.php
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
/**
|
||||
* @see https://w3c.github.io/webappsec-credential-management/#credential
|
||||
*/
|
||||
abstract class Credential
|
||||
{
|
||||
public function __construct(
|
||||
protected string $id,
|
||||
protected string $type
|
||||
) {
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
||||
21
libraries/vendor/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php
vendored
Normal file
21
libraries/vendor/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Event;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationObject;
|
||||
use Webauthn\MetadataService\Event\WebauthnEvent;
|
||||
|
||||
class AttestationObjectLoaded implements WebauthnEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly AttestationObject $attestationObject
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(AttestationObject $attestationObject): self
|
||||
{
|
||||
return new self($attestationObject);
|
||||
}
|
||||
}
|
||||
21
libraries/vendor/web-auth/webauthn-lib/src/Event/AttestationStatementLoaded.php
vendored
Normal file
21
libraries/vendor/web-auth/webauthn-lib/src/Event/AttestationStatementLoaded.php
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Event;
|
||||
|
||||
use Webauthn\AttestationStatement\AttestationStatement;
|
||||
use Webauthn\MetadataService\Event\WebauthnEvent;
|
||||
|
||||
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,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
|
||||
class AuthenticatorAssertionResponseValidationFailedEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $credentialId,
|
||||
private readonly AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
private readonly PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
public readonly ServerRequestInterface|string $host,
|
||||
private readonly ?string $userHandle,
|
||||
private 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCredentialId(): string
|
||||
{
|
||||
return $this->credentialId;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public function getRequest(): ServerRequestInterface|string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getUserHandle(): ?string
|
||||
{
|
||||
return $this->userHandle;
|
||||
}
|
||||
|
||||
public function getThrowable(): Throwable
|
||||
{
|
||||
return $this->throwable;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
<?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(
|
||||
private readonly string $credentialId,
|
||||
private readonly AuthenticatorAssertionResponse $authenticatorAssertionResponse,
|
||||
private readonly PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
public readonly ServerRequestInterface|string $host,
|
||||
private readonly ?string $userHandle,
|
||||
private 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCredentialId(): string
|
||||
{
|
||||
return $this->credentialId;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public function getRequest(): ServerRequestInterface|string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getUserHandle(): ?string
|
||||
{
|
||||
return $this->userHandle;
|
||||
}
|
||||
|
||||
public function getPublicKeyCredentialSource(): PublicKeyCredentialSource
|
||||
{
|
||||
return $this->publicKeyCredentialSource;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
<?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(
|
||||
private readonly AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
private readonly PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
public readonly ServerRequestInterface|string $host,
|
||||
private 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAuthenticatorAttestationResponse(): AuthenticatorAttestationResponse
|
||||
{
|
||||
return $this->authenticatorAttestationResponse;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public function getRequest(): ServerRequestInterface|string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getThrowable(): Throwable
|
||||
{
|
||||
return $this->throwable;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
<?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(
|
||||
private readonly AuthenticatorAttestationResponse $authenticatorAttestationResponse,
|
||||
private readonly PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
public readonly ServerRequestInterface|string $host,
|
||||
private 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAuthenticatorAttestationResponse(): AuthenticatorAttestationResponse
|
||||
{
|
||||
return $this->authenticatorAttestationResponse;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public function getRequest(): ServerRequestInterface|string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getPublicKeyCredentialSource(): PublicKeyCredentialSource
|
||||
{
|
||||
return $this->publicKeyCredentialSource;
|
||||
}
|
||||
}
|
||||
9
libraries/vendor/web-auth/webauthn-lib/src/Exception/AttestationStatementException.php
vendored
Normal file
9
libraries/vendor/web-auth/webauthn-lib/src/Exception/AttestationStatementException.php
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
class AttestationStatementException extends WebauthnException
|
||||
{
|
||||
}
|
||||
32
libraries/vendor/web-auth/webauthn-lib/src/Exception/AttestationStatementLoadingException.php
vendored
Normal file
32
libraries/vendor/web-auth/webauthn-lib/src/Exception/AttestationStatementLoadingException.php
vendored
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
15
libraries/vendor/web-auth/webauthn-lib/src/Exception/AuthenticationExtensionException.php
vendored
Normal file
15
libraries/vendor/web-auth/webauthn-lib/src/Exception/AuthenticationExtensionException.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
final class AuthenticationExtensionException extends WebauthnException
|
||||
{
|
||||
public static function create(string $message, ?Throwable $previous = null): self
|
||||
{
|
||||
return new self($message, $previous);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
final class AuthenticatorResponseVerificationException extends WebauthnException
|
||||
{
|
||||
public static function create(string $message, ?Throwable $previous = null): self
|
||||
{
|
||||
return new self($message, $previous);
|
||||
}
|
||||
}
|
||||
28
libraries/vendor/web-auth/webauthn-lib/src/Exception/CounterException.php
vendored
Normal file
28
libraries/vendor/web-auth/webauthn-lib/src/Exception/CounterException.php
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
final class CounterException extends WebauthnException
|
||||
{
|
||||
public function __construct(
|
||||
public int $currentCounter,
|
||||
public int $authenticatorCounter,
|
||||
string $message,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $previous);
|
||||
}
|
||||
|
||||
public static function create(
|
||||
int $currentCounter,
|
||||
int $authenticatorCounter,
|
||||
string $message,
|
||||
?Throwable $previous = null
|
||||
): self {
|
||||
return new self($currentCounter, $authenticatorCounter, $message, $previous);
|
||||
}
|
||||
}
|
||||
27
libraries/vendor/web-auth/webauthn-lib/src/Exception/InvalidAttestationStatementException.php
vendored
Normal file
27
libraries/vendor/web-auth/webauthn-lib/src/Exception/InvalidAttestationStatementException.php
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
use Webauthn\AttestationStatement\AttestationStatement;
|
||||
|
||||
final class InvalidAttestationStatementException extends AttestationStatementException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly AttestationStatement $attestationStatement,
|
||||
string $message,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $previous);
|
||||
}
|
||||
|
||||
public static function create(
|
||||
AttestationStatement $attestationStatement,
|
||||
string $message = 'Invalid attestation statement',
|
||||
?Throwable $previous = null
|
||||
): self {
|
||||
return new self($attestationStatement, $message, $previous);
|
||||
}
|
||||
}
|
||||
23
libraries/vendor/web-auth/webauthn-lib/src/Exception/InvalidDataException.php
vendored
Normal file
23
libraries/vendor/web-auth/webauthn-lib/src/Exception/InvalidDataException.php
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
final class InvalidDataException extends WebauthnException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly mixed $data,
|
||||
string $message,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $previous);
|
||||
}
|
||||
|
||||
public static function create(mixed $data, string $message, ?Throwable $previous = null): self
|
||||
{
|
||||
return new self($data, $message, $previous);
|
||||
}
|
||||
}
|
||||
15
libraries/vendor/web-auth/webauthn-lib/src/Exception/InvalidTrustPathException.php
vendored
Normal file
15
libraries/vendor/web-auth/webauthn-lib/src/Exception/InvalidTrustPathException.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
final class InvalidTrustPathException extends WebauthnException
|
||||
{
|
||||
public static function create(string $message, ?Throwable $previous = null): self
|
||||
{
|
||||
return new self($message, $previous);
|
||||
}
|
||||
}
|
||||
15
libraries/vendor/web-auth/webauthn-lib/src/Exception/UnsupportedFeatureException.php
vendored
Normal file
15
libraries/vendor/web-auth/webauthn-lib/src/Exception/UnsupportedFeatureException.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
final class UnsupportedFeatureException extends WebauthnException
|
||||
{
|
||||
public static function create(string $message, ?Throwable $previous = null): self
|
||||
{
|
||||
return new self($message, $previous);
|
||||
}
|
||||
}
|
||||
16
libraries/vendor/web-auth/webauthn-lib/src/Exception/WebauthnException.php
vendored
Normal file
16
libraries/vendor/web-auth/webauthn-lib/src/Exception/WebauthnException.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Exception;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class WebauthnException extends Exception
|
||||
{
|
||||
public function __construct(string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
46
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredential.php
vendored
Normal file
46
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredential.php
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#iface-pkcredential
|
||||
*/
|
||||
class PublicKeyCredential extends Credential implements Stringable
|
||||
{
|
||||
public function __construct(
|
||||
string $id,
|
||||
string $type,
|
||||
protected string $rawId,
|
||||
protected AuthenticatorResponse $response
|
||||
) {
|
||||
parent::__construct($id, $type);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return json_encode($this, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function getRawId(): string
|
||||
{
|
||||
return $this->rawId;
|
||||
}
|
||||
|
||||
public function getResponse(): AuthenticatorResponse
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $transport
|
||||
*/
|
||||
public function getPublicKeyCredentialDescriptor(array $transport = []): PublicKeyCredentialDescriptor
|
||||
{
|
||||
return new PublicKeyCredentialDescriptor($this->getType(), $this->getRawId(), $transport);
|
||||
}
|
||||
}
|
||||
257
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php
vendored
Normal file
257
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php
vendored
Normal file
@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\Util\Base64;
|
||||
|
||||
final class PublicKeyCredentialCreationOptions extends PublicKeyCredentialOptions
|
||||
{
|
||||
public const ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 'none';
|
||||
|
||||
public const ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 'indirect';
|
||||
|
||||
public const ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 'direct';
|
||||
|
||||
public const ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE = 'enterprise';
|
||||
|
||||
/**
|
||||
* @var PublicKeyCredentialDescriptor[]
|
||||
*/
|
||||
private array $excludeCredentials = [];
|
||||
|
||||
private ?AuthenticatorSelectionCriteria $authenticatorSelection = null;
|
||||
|
||||
private ?string $attestation = null;
|
||||
|
||||
/**
|
||||
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly PublicKeyCredentialRpEntity $rp,
|
||||
private readonly PublicKeyCredentialUserEntity $user,
|
||||
string $challenge,
|
||||
private array $pubKeyCredParams
|
||||
) {
|
||||
parent::__construct($challenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PublicKeyCredentialParameters[] $pubKeyCredParams
|
||||
*/
|
||||
public static function create(
|
||||
PublicKeyCredentialRpEntity $rp,
|
||||
PublicKeyCredentialUserEntity $user,
|
||||
string $challenge,
|
||||
array $pubKeyCredParams
|
||||
): self {
|
||||
return new self($rp, $user, $challenge, $pubKeyCredParams);
|
||||
}
|
||||
|
||||
public function addPubKeyCredParam(PublicKeyCredentialParameters $pubKeyCredParam): self
|
||||
{
|
||||
$this->pubKeyCredParams[] = $pubKeyCredParam;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addPubKeyCredParams(PublicKeyCredentialParameters ...$pubKeyCredParams): self
|
||||
{
|
||||
foreach ($pubKeyCredParams as $pubKeyCredParam) {
|
||||
$this->addPubKeyCredParam($pubKeyCredParam);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function excludeCredential(PublicKeyCredentialDescriptor $excludeCredential): self
|
||||
{
|
||||
$this->excludeCredentials[] = $excludeCredential;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function excludeCredentials(PublicKeyCredentialDescriptor ...$excludeCredentials): self
|
||||
{
|
||||
foreach ($excludeCredentials as $excludeCredential) {
|
||||
$this->excludeCredential($excludeCredential);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAuthenticatorSelection(?AuthenticatorSelectionCriteria $authenticatorSelection): self
|
||||
{
|
||||
$this->authenticatorSelection = $authenticatorSelection;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAttestation(string $attestation): self
|
||||
{
|
||||
in_array($attestation, [
|
||||
self::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
|
||||
self::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT,
|
||||
self::ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT,
|
||||
self::ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE,
|
||||
], true) || throw InvalidDataException::create($attestation, 'Invalid attestation conveyance mode');
|
||||
$this->attestation = $attestation;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRp(): PublicKeyCredentialRpEntity
|
||||
{
|
||||
return $this->rp;
|
||||
}
|
||||
|
||||
public function getUser(): PublicKeyCredentialUserEntity
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PublicKeyCredentialParameters[]
|
||||
*/
|
||||
public function getPubKeyCredParams(): array
|
||||
{
|
||||
return $this->pubKeyCredParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PublicKeyCredentialDescriptor[]
|
||||
*/
|
||||
public function getExcludeCredentials(): array
|
||||
{
|
||||
return $this->excludeCredentials;
|
||||
}
|
||||
|
||||
public function getAuthenticatorSelection(): ?AuthenticatorSelectionCriteria
|
||||
{
|
||||
return $this->authenticatorSelection;
|
||||
}
|
||||
|
||||
public function getAttestation(): ?string
|
||||
{
|
||||
return $this->attestation;
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): static
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
public static function createFromArray(array $json): static
|
||||
{
|
||||
array_key_exists('rp', $json) || throw InvalidDataException::create($json, 'Invalid input. "rp" is missing.');
|
||||
array_key_exists('pubKeyCredParams', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "pubKeyCredParams" is missing.'
|
||||
);
|
||||
is_array($json['pubKeyCredParams']) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "pubKeyCredParams" is not an array.'
|
||||
);
|
||||
array_key_exists('challenge', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "challenge" is missing.'
|
||||
);
|
||||
array_key_exists('attestation', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "attestation" is missing.'
|
||||
);
|
||||
array_key_exists('user', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "user" is missing.'
|
||||
);
|
||||
|
||||
$pubKeyCredParams = [];
|
||||
foreach ($json['pubKeyCredParams'] as $pubKeyCredParam) {
|
||||
if (! is_array($pubKeyCredParam)) {
|
||||
continue;
|
||||
}
|
||||
$pubKeyCredParams[] = PublicKeyCredentialParameters::createFromArray($pubKeyCredParam);
|
||||
}
|
||||
$excludeCredentials = [];
|
||||
if (isset($json['excludeCredentials'])) {
|
||||
foreach ($json['excludeCredentials'] as $excludeCredential) {
|
||||
$excludeCredentials[] = PublicKeyCredentialDescriptor::createFromArray($excludeCredential);
|
||||
}
|
||||
}
|
||||
|
||||
$challenge = Base64::decode($json['challenge']);
|
||||
|
||||
return self
|
||||
::create(
|
||||
PublicKeyCredentialRpEntity::createFromArray($json['rp']),
|
||||
PublicKeyCredentialUserEntity::createFromArray($json['user']),
|
||||
$challenge,
|
||||
$pubKeyCredParams
|
||||
)
|
||||
->setTimeout($json['timeout'] ?? null)
|
||||
->excludeCredentials(...$excludeCredentials)
|
||||
->setAuthenticatorSelection(
|
||||
isset($json['authenticatorSelection']) ? AuthenticatorSelectionCriteria::createFromArray(
|
||||
$json['authenticatorSelection']
|
||||
) : null
|
||||
)
|
||||
->setAttestation($json['attestation'] ?? null)
|
||||
->setExtensions(
|
||||
isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray(
|
||||
$json['extensions']
|
||||
) : new AuthenticationExtensionsClientInputs()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = [
|
||||
'rp' => $this->rp->jsonSerialize(),
|
||||
'user' => $this->user->jsonSerialize(),
|
||||
'challenge' => Base64UrlSafe::encodeUnpadded($this->challenge),
|
||||
'pubKeyCredParams' => array_map(
|
||||
static fn (PublicKeyCredentialParameters $object): array => $object->jsonSerialize(),
|
||||
$this->pubKeyCredParams
|
||||
),
|
||||
];
|
||||
|
||||
if ($this->timeout !== null) {
|
||||
$json['timeout'] = $this->timeout;
|
||||
}
|
||||
|
||||
if (count($this->excludeCredentials) !== 0) {
|
||||
$json['excludeCredentials'] = array_map(
|
||||
static fn (PublicKeyCredentialDescriptor $object): array => $object->jsonSerialize(),
|
||||
$this->excludeCredentials
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->authenticatorSelection !== null) {
|
||||
$json['authenticatorSelection'] = $this->authenticatorSelection->jsonSerialize();
|
||||
}
|
||||
|
||||
if ($this->attestation !== null) {
|
||||
$json['attestation'] = $this->attestation;
|
||||
}
|
||||
|
||||
if ($this->extensions->count() !== 0) {
|
||||
$json['extensions'] = $this->extensions;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
102
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php
vendored
Normal file
102
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use JsonSerializable;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
class PublicKeyCredentialDescriptor implements JsonSerializable
|
||||
{
|
||||
final public const CREDENTIAL_TYPE_PUBLIC_KEY = 'public-key';
|
||||
|
||||
final public const AUTHENTICATOR_TRANSPORT_USB = 'usb';
|
||||
|
||||
final public const AUTHENTICATOR_TRANSPORT_NFC = 'nfc';
|
||||
|
||||
final public const AUTHENTICATOR_TRANSPORT_BLE = 'ble';
|
||||
|
||||
final public const AUTHENTICATOR_TRANSPORT_CABLE = 'cable';
|
||||
|
||||
final public const AUTHENTICATOR_TRANSPORT_INTERNAL = 'internal';
|
||||
|
||||
/**
|
||||
* @param string[] $transports
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $type,
|
||||
protected string $id,
|
||||
protected array $transports = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $transports
|
||||
*/
|
||||
public static function create(string $type, string $id, array $transports = []): self
|
||||
{
|
||||
return new self($type, $id, $transports);
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTransports(): array
|
||||
{
|
||||
return $this->transports;
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
array_key_exists('type', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "type" is missing.'
|
||||
);
|
||||
array_key_exists('id', $json) || throw InvalidDataException::create($json, 'Invalid input. "id" is missing.');
|
||||
|
||||
$id = Base64UrlSafe::decodeNoPadding($json['id']);
|
||||
|
||||
return new self($json['type'], $id, $json['transports'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = [
|
||||
'type' => $this->type,
|
||||
'id' => Base64UrlSafe::encodeUnpadded($this->id),
|
||||
];
|
||||
if (count($this->transports) !== 0) {
|
||||
$json['transports'] = $this->transports;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
95
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php
vendored
Normal file
95
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use ArrayIterator;
|
||||
use function count;
|
||||
use const COUNT_NORMAL;
|
||||
use Countable;
|
||||
use function is_array;
|
||||
use Iterator;
|
||||
use IteratorAggregate;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<PublicKeyCredentialDescriptor>
|
||||
*/
|
||||
class PublicKeyCredentialDescriptorCollection implements JsonSerializable, Countable, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @var PublicKeyCredentialDescriptor[]
|
||||
*/
|
||||
private array $publicKeyCredentialDescriptors = [];
|
||||
|
||||
public function add(PublicKeyCredentialDescriptor ...$publicKeyCredentialDescriptors): void
|
||||
{
|
||||
foreach ($publicKeyCredentialDescriptors as $publicKeyCredentialDescriptor) {
|
||||
$this->publicKeyCredentialDescriptors[$publicKeyCredentialDescriptor->getId()] = $publicKeyCredentialDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return array_key_exists($id, $this->publicKeyCredentialDescriptors);
|
||||
}
|
||||
|
||||
public function remove(string $id): void
|
||||
{
|
||||
if (! $this->has($id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->publicKeyCredentialDescriptors[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Iterator<string, PublicKeyCredentialDescriptor>
|
||||
*/
|
||||
public function getIterator(): Iterator
|
||||
{
|
||||
return new ArrayIterator($this->publicKeyCredentialDescriptors);
|
||||
}
|
||||
|
||||
public function count(int $mode = COUNT_NORMAL): int
|
||||
{
|
||||
return count($this->publicKeyCredentialDescriptors, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (PublicKeyCredentialDescriptor $object): array => $object->jsonSerialize(),
|
||||
$this->publicKeyCredentialDescriptors
|
||||
);
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
$collection = new self();
|
||||
foreach ($json as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$collection->add(PublicKeyCredentialDescriptor::createFromArray($item));
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
41
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php
vendored
Normal file
41
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
abstract class PublicKeyCredentialEntity implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
protected string $name,
|
||||
protected ?string $icon
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getIcon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = [
|
||||
'name' => $this->name,
|
||||
];
|
||||
if ($this->icon !== null) {
|
||||
$json['icon'] = $this->icon;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
221
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php
vendored
Normal file
221
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php
vendored
Normal file
@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use function ord;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Throwable;
|
||||
use function unpack;
|
||||
use Webauthn\AttestationStatement\AttestationObjectLoader;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\MetadataService\CanLogData;
|
||||
use Webauthn\Util\Base64;
|
||||
|
||||
class PublicKeyCredentialLoader implements CanLogData
|
||||
{
|
||||
private const FLAG_AT = 0b01000000;
|
||||
|
||||
private const FLAG_ED = 0b10000000;
|
||||
|
||||
private readonly Decoder $decoder;
|
||||
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
private readonly AttestationObjectLoader $attestationObjectLoader
|
||||
) {
|
||||
$this->decoder = Decoder::create();
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
public static function create(AttestationObjectLoader $attestationObjectLoader): self
|
||||
{
|
||||
return new self($attestationObjectLoader);
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public function loadArray(array $json): PublicKeyCredential
|
||||
{
|
||||
$this->logger->info('Trying to load data from an array', [
|
||||
'data' => $json,
|
||||
]);
|
||||
try {
|
||||
foreach (['id', 'rawId', 'type'] as $key) {
|
||||
array_key_exists($key, $json) || throw InvalidDataException::create($json, sprintf(
|
||||
'The parameter "%s" is missing',
|
||||
$key
|
||||
));
|
||||
is_string($json[$key]) || throw InvalidDataException::create($json, sprintf(
|
||||
'The parameter "%s" shall be a string',
|
||||
$key
|
||||
));
|
||||
}
|
||||
array_key_exists('response', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'The parameter "response" is missing'
|
||||
);
|
||||
is_array($json['response']) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'The parameter "response" shall be an array'
|
||||
);
|
||||
$json['type'] === 'public-key' || throw InvalidDataException::create($json, sprintf(
|
||||
'Unsupported type "%s"',
|
||||
$json['type']
|
||||
));
|
||||
|
||||
$id = Base64UrlSafe::decodeNoPadding($json['id']);
|
||||
$rawId = Base64::decode($json['rawId']);
|
||||
hash_equals($id, $rawId) || throw InvalidDataException::create($json, 'Invalid ID');
|
||||
|
||||
$publicKeyCredential = new PublicKeyCredential(
|
||||
$json['id'],
|
||||
$json['type'],
|
||||
$rawId,
|
||||
$this->createResponse($json['response'])
|
||||
);
|
||||
$this->logger->info('The data has been loaded');
|
||||
$this->logger->debug('Public Key Credential', [
|
||||
'publicKeyCredential' => $publicKeyCredential,
|
||||
]);
|
||||
|
||||
return $publicKeyCredential;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function load(string $data): PublicKeyCredential
|
||||
{
|
||||
$this->logger->info('Trying to load data from a string', [
|
||||
'data' => $data,
|
||||
]);
|
||||
try {
|
||||
$json = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return $this->loadArray($json);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
throw InvalidDataException::create($data, 'Unable to load the data', $throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $response
|
||||
*/
|
||||
private function createResponse(array $response): AuthenticatorResponse
|
||||
{
|
||||
array_key_exists('clientDataJSON', $response) || throw InvalidDataException::create(
|
||||
$response,
|
||||
'Invalid data. The parameter "clientDataJSON" is missing'
|
||||
);
|
||||
is_string($response['clientDataJSON']) || throw InvalidDataException::create(
|
||||
$response,
|
||||
'Invalid data. The parameter "clientDataJSON" is invalid'
|
||||
);
|
||||
$userHandle = $response['userHandle'] ?? null;
|
||||
$userHandle === null || is_string($userHandle) || throw InvalidDataException::create(
|
||||
$response,
|
||||
'Invalid data. The parameter "userHandle" is invalid'
|
||||
);
|
||||
switch (true) {
|
||||
case array_key_exists('attestationObject', $response):
|
||||
is_string($response['attestationObject']) || throw InvalidDataException::create(
|
||||
$response,
|
||||
'Invalid data. The parameter "attestationObject " is invalid'
|
||||
);
|
||||
$attestationObject = $this->attestationObjectLoader->load($response['attestationObject']);
|
||||
|
||||
return new AuthenticatorAttestationResponse(CollectedClientData::createFormJson(
|
||||
$response['clientDataJSON']
|
||||
), $attestationObject);
|
||||
case array_key_exists('authenticatorData', $response) && array_key_exists('signature', $response):
|
||||
$authData = Base64UrlSafe::decodeNoPadding($response['authenticatorData']);
|
||||
|
||||
$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) & self::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 = new AttestedCredentialData(
|
||||
$aaguid,
|
||||
$credentialId,
|
||||
(string) $credentialPublicKey
|
||||
);
|
||||
}
|
||||
|
||||
$extension = null;
|
||||
if (0 !== (ord($flags) & self::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();
|
||||
$authenticatorData = new AuthenticatorData(
|
||||
$authData,
|
||||
$rp_id_hash,
|
||||
$flags,
|
||||
$signCount[1],
|
||||
$attestedCredentialData,
|
||||
$extension
|
||||
);
|
||||
|
||||
try {
|
||||
$signature = Base64::decode($response['signature']);
|
||||
} catch (Throwable $e) {
|
||||
throw InvalidDataException::create(
|
||||
$response['signature'],
|
||||
'The signature shall be Base64 Url Safe encoded',
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
return new AuthenticatorAssertionResponse(
|
||||
CollectedClientData::createFormJson($response['clientDataJSON']),
|
||||
$authenticatorData,
|
||||
$signature,
|
||||
$response['userHandle'] ?? null
|
||||
);
|
||||
default:
|
||||
throw InvalidDataException::create($response, 'Unable to create the response object');
|
||||
}
|
||||
}
|
||||
}
|
||||
77
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php
vendored
Normal file
77
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use JsonSerializable;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtension;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
|
||||
|
||||
abstract class PublicKeyCredentialOptions implements JsonSerializable
|
||||
{
|
||||
protected ?int $timeout = null;
|
||||
|
||||
protected AuthenticationExtensionsClientInputs $extensions;
|
||||
|
||||
public function __construct(
|
||||
protected string $challenge
|
||||
) {
|
||||
$this->extensions = new AuthenticationExtensionsClientInputs();
|
||||
}
|
||||
|
||||
public function setTimeout(?int $timeout): static
|
||||
{
|
||||
$this->timeout = $timeout;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addExtension(AuthenticationExtension $extension): static
|
||||
{
|
||||
$this->extensions->add($extension);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthenticationExtension[] $extensions
|
||||
*/
|
||||
public function addExtensions(array $extensions): static
|
||||
{
|
||||
foreach ($extensions as $extension) {
|
||||
$this->addExtension($extension);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setExtensions(AuthenticationExtensionsClientInputs $extensions): static
|
||||
{
|
||||
$this->extensions = $extensions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getChallenge(): string
|
||||
{
|
||||
return $this->challenge;
|
||||
}
|
||||
|
||||
public function getTimeout(): ?int
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
|
||||
public function getExtensions(): AuthenticationExtensionsClientInputs
|
||||
{
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
abstract public static function createFromString(string $data): static;
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
abstract public static function createFromArray(array $json): static;
|
||||
}
|
||||
69
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php
vendored
Normal file
69
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use JsonSerializable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
class PublicKeyCredentialParameters implements JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $type,
|
||||
private readonly int $alg
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(string $type, int $alg): self
|
||||
{
|
||||
return new self($type, $alg);
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getAlg(): int
|
||||
{
|
||||
return $this->alg;
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
array_key_exists('type', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "type" is missing.'
|
||||
);
|
||||
array_key_exists('alg', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "alg" is missing.'
|
||||
);
|
||||
|
||||
return new self($json['type'], $json['alg']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type,
|
||||
'alg' => $this->alg,
|
||||
];
|
||||
}
|
||||
}
|
||||
167
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php
vendored
Normal file
167
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php
vendored
Normal file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\Util\Base64;
|
||||
|
||||
final class PublicKeyCredentialRequestOptions extends PublicKeyCredentialOptions
|
||||
{
|
||||
public const USER_VERIFICATION_REQUIREMENT_REQUIRED = 'required';
|
||||
|
||||
public const USER_VERIFICATION_REQUIREMENT_PREFERRED = 'preferred';
|
||||
|
||||
public const USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 'discouraged';
|
||||
|
||||
private ?string $rpId = null;
|
||||
|
||||
/**
|
||||
* @var PublicKeyCredentialDescriptor[]
|
||||
*/
|
||||
private array $allowCredentials = [];
|
||||
|
||||
private ?string $userVerification = null;
|
||||
|
||||
public static function create(string $challenge): self
|
||||
{
|
||||
return new self($challenge);
|
||||
}
|
||||
|
||||
public function setRpId(?string $rpId): self
|
||||
{
|
||||
$this->rpId = $rpId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function allowCredential(PublicKeyCredentialDescriptor $allowCredential): self
|
||||
{
|
||||
$this->allowCredentials[] = $allowCredential;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function allowCredentials(PublicKeyCredentialDescriptor ...$allowCredentials): self
|
||||
{
|
||||
foreach ($allowCredentials as $allowCredential) {
|
||||
$this->allowCredential($allowCredential);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUserVerification(?string $userVerification): self
|
||||
{
|
||||
if ($userVerification === null) {
|
||||
$this->rpId = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
in_array($userVerification, [
|
||||
self::USER_VERIFICATION_REQUIREMENT_REQUIRED,
|
||||
self::USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
||||
self::USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
|
||||
], true) || throw InvalidDataException::create($userVerification, 'Invalid user verification requirement');
|
||||
$this->userVerification = $userVerification;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRpId(): ?string
|
||||
{
|
||||
return $this->rpId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PublicKeyCredentialDescriptor[]
|
||||
*/
|
||||
public function getAllowCredentials(): array
|
||||
{
|
||||
return $this->allowCredentials;
|
||||
}
|
||||
|
||||
public function getUserVerification(): ?string
|
||||
{
|
||||
return $this->userVerification;
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): static
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): static
|
||||
{
|
||||
array_key_exists('challenge', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "challenge" is missing.'
|
||||
);
|
||||
|
||||
$allowCredentials = [];
|
||||
$allowCredentialList = $json['allowCredentials'] ?? [];
|
||||
foreach ($allowCredentialList as $allowCredential) {
|
||||
$allowCredentials[] = PublicKeyCredentialDescriptor::createFromArray($allowCredential);
|
||||
}
|
||||
|
||||
$challenge = Base64::decode($json['challenge']);
|
||||
|
||||
return self::create($challenge)
|
||||
->setRpId($json['rpId'] ?? null)
|
||||
->allowCredentials(...$allowCredentials)
|
||||
->setUserVerification($json['userVerification'] ?? null)
|
||||
->setTimeout($json['timeout'] ?? null)
|
||||
->setExtensions(
|
||||
isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray(
|
||||
$json['extensions']
|
||||
) : new AuthenticationExtensionsClientInputs()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = [
|
||||
'challenge' => Base64UrlSafe::encodeUnpadded($this->challenge),
|
||||
];
|
||||
|
||||
if ($this->rpId !== null) {
|
||||
$json['rpId'] = $this->rpId;
|
||||
}
|
||||
|
||||
if ($this->userVerification !== null) {
|
||||
$json['userVerification'] = $this->userVerification;
|
||||
}
|
||||
|
||||
if (count($this->allowCredentials) !== 0) {
|
||||
$json['allowCredentials'] = array_map(
|
||||
static fn (PublicKeyCredentialDescriptor $object): array => $object->jsonSerialize(),
|
||||
$this->allowCredentials
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->extensions->count() !== 0) {
|
||||
$json['extensions'] = $this->extensions->jsonSerialize();
|
||||
}
|
||||
|
||||
if ($this->timeout !== null) {
|
||||
$json['timeout'] = $this->timeout;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
55
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php
vendored
Normal file
55
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
class PublicKeyCredentialRpEntity extends PublicKeyCredentialEntity
|
||||
{
|
||||
public function __construct(
|
||||
string $name,
|
||||
protected ?string $id = null,
|
||||
?string $icon = null
|
||||
) {
|
||||
parent::__construct($name, $icon);
|
||||
}
|
||||
|
||||
public static function create(string $name, ?string $id = null, ?string $icon = null): self
|
||||
{
|
||||
return new self($name, $id, $icon);
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
array_key_exists('name', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "name" is missing.'
|
||||
);
|
||||
|
||||
return new self($json['name'], $json['id'] ?? null, $json['icon'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = parent::jsonSerialize();
|
||||
if ($this->id !== null) {
|
||||
$json['id'] = $this->id;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
208
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php
vendored
Normal file
208
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php
vendored
Normal file
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use JsonSerializable;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Throwable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
use Webauthn\TrustPath\TrustPath;
|
||||
use Webauthn\TrustPath\TrustPathLoader;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/webauthn/#iface-pkcredential
|
||||
*/
|
||||
class PublicKeyCredentialSource implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param string[] $transports
|
||||
* @param array<string, mixed>|null $otherUI
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $publicKeyCredentialId,
|
||||
protected string $type,
|
||||
protected array $transports,
|
||||
protected string $attestationType,
|
||||
protected TrustPath $trustPath,
|
||||
protected AbstractUid $aaguid,
|
||||
protected string $credentialPublicKey,
|
||||
protected string $userHandle,
|
||||
protected int $counter,
|
||||
protected ?array $otherUI = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $transports
|
||||
* @param array<string, mixed>|null $otherUI
|
||||
*/
|
||||
public static function create(
|
||||
string $publicKeyCredentialId,
|
||||
string $type,
|
||||
array $transports,
|
||||
string $attestationType,
|
||||
TrustPath $trustPath,
|
||||
AbstractUid $aaguid,
|
||||
string $credentialPublicKey,
|
||||
string $userHandle,
|
||||
int $counter,
|
||||
?array $otherUI = null
|
||||
): self {
|
||||
return new self(
|
||||
$publicKeyCredentialId,
|
||||
$type,
|
||||
$transports,
|
||||
$attestationType,
|
||||
$trustPath,
|
||||
$aaguid,
|
||||
$credentialPublicKey,
|
||||
$userHandle,
|
||||
$counter,
|
||||
$otherUI
|
||||
);
|
||||
}
|
||||
|
||||
public function getPublicKeyCredentialId(): string
|
||||
{
|
||||
return $this->publicKeyCredentialId;
|
||||
}
|
||||
|
||||
public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor
|
||||
{
|
||||
return new PublicKeyCredentialDescriptor($this->type, $this->publicKeyCredentialId, $this->transports);
|
||||
}
|
||||
|
||||
public function getAttestationType(): string
|
||||
{
|
||||
return $this->attestationType;
|
||||
}
|
||||
|
||||
public function getTrustPath(): TrustPath
|
||||
{
|
||||
return $this->trustPath;
|
||||
}
|
||||
|
||||
public function getAttestedCredentialData(): AttestedCredentialData
|
||||
{
|
||||
return new AttestedCredentialData($this->aaguid, $this->publicKeyCredentialId, $this->credentialPublicKey);
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTransports(): array
|
||||
{
|
||||
return $this->transports;
|
||||
}
|
||||
|
||||
public function getAaguid(): AbstractUid
|
||||
{
|
||||
return $this->aaguid;
|
||||
}
|
||||
|
||||
public function getCredentialPublicKey(): string
|
||||
{
|
||||
return $this->credentialPublicKey;
|
||||
}
|
||||
|
||||
public function getUserHandle(): string
|
||||
{
|
||||
return $this->userHandle;
|
||||
}
|
||||
|
||||
public function getCounter(): int
|
||||
{
|
||||
return $this->counter;
|
||||
}
|
||||
|
||||
public function setCounter(int $counter): void
|
||||
{
|
||||
$this->counter = $counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function getOtherUI(): ?array
|
||||
{
|
||||
return $this->otherUI;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $otherUI
|
||||
*/
|
||||
public function setOtherUI(?array $otherUI): self
|
||||
{
|
||||
$this->otherUI = $otherUI;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public static function createFromArray(array $data): self
|
||||
{
|
||||
$keys = array_keys(get_class_vars(self::class));
|
||||
foreach ($keys as $key) {
|
||||
if ($key === 'otherUI') {
|
||||
continue;
|
||||
}
|
||||
array_key_exists($key, $data) || throw InvalidDataException::create($data, sprintf(
|
||||
'The parameter "%s" is missing',
|
||||
$key
|
||||
));
|
||||
}
|
||||
mb_strlen((string) $data['aaguid'], '8bit') === 36 || throw InvalidDataException::create(
|
||||
$data,
|
||||
'Invalid AAGUID'
|
||||
);
|
||||
$uuid = Uuid::fromString($data['aaguid']);
|
||||
|
||||
try {
|
||||
return new self(
|
||||
Base64UrlSafe::decodeNoPadding($data['publicKeyCredentialId']),
|
||||
$data['type'],
|
||||
$data['transports'],
|
||||
$data['attestationType'],
|
||||
TrustPathLoader::loadTrustPath($data['trustPath']),
|
||||
$uuid,
|
||||
Base64UrlSafe::decodeNoPadding($data['credentialPublicKey']),
|
||||
Base64UrlSafe::decodeNoPadding($data['userHandle']),
|
||||
$data['counter'],
|
||||
$data['otherUI'] ?? null
|
||||
);
|
||||
} catch (Throwable $throwable) {
|
||||
throw InvalidDataException::create($data, 'Unable to load the data', $throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'publicKeyCredentialId' => Base64UrlSafe::encodeUnpadded($this->publicKeyCredentialId),
|
||||
'type' => $this->type,
|
||||
'transports' => $this->transports,
|
||||
'attestationType' => $this->attestationType,
|
||||
'trustPath' => $this->trustPath->jsonSerialize(),
|
||||
'aaguid' => $this->aaguid->__toString(),
|
||||
'credentialPublicKey' => Base64UrlSafe::encodeUnpadded($this->credentialPublicKey),
|
||||
'userHandle' => Base64UrlSafe::encodeUnpadded($this->userHandle),
|
||||
'counter' => $this->counter,
|
||||
'otherUI' => $this->otherUI,
|
||||
];
|
||||
}
|
||||
}
|
||||
17
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php
vendored
Normal file
17
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
interface PublicKeyCredentialSourceRepository
|
||||
{
|
||||
public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource;
|
||||
|
||||
/**
|
||||
* @return PublicKeyCredentialSource[]
|
||||
*/
|
||||
public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array;
|
||||
|
||||
public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void;
|
||||
}
|
||||
82
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialUserEntity.php
vendored
Normal file
82
libraries/vendor/web-auth/webauthn-lib/src/PublicKeyCredentialUserEntity.php
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use ParagonIE\ConstantTime\Base64;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
class PublicKeyCredentialUserEntity extends PublicKeyCredentialEntity
|
||||
{
|
||||
protected string $id;
|
||||
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $id,
|
||||
protected string $displayName,
|
||||
?string $icon = null
|
||||
) {
|
||||
parent::__construct($name, $icon);
|
||||
mb_strlen($id, '8bit') <= 64 || throw InvalidDataException::create($id, 'User ID max length is 64 bytes');
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public static function create(string $name, string $id, string $displayName, ?string $icon = null): self
|
||||
{
|
||||
return new self($name, $id, $displayName, $icon);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return $this->displayName;
|
||||
}
|
||||
|
||||
public static function createFromString(string $data): self
|
||||
{
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
is_array($data) || throw InvalidDataException::create($data, 'Invalid data');
|
||||
|
||||
return self::createFromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFromArray(array $json): self
|
||||
{
|
||||
array_key_exists('name', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "name" is missing.'
|
||||
);
|
||||
array_key_exists('id', $json) || throw InvalidDataException::create($json, 'Invalid input. "id" is missing.');
|
||||
array_key_exists('displayName', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'Invalid input. "displayName" is missing.'
|
||||
);
|
||||
$id = Base64::decode($json['id'], true);
|
||||
|
||||
return new self($json['name'], $id, $json['displayName'], $json['icon'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$json = parent::jsonSerialize();
|
||||
$json['id'] = Base64UrlSafe::encodeUnpadded($this->id);
|
||||
$json['displayName'] = $this->displayName;
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
61
libraries/vendor/web-auth/webauthn-lib/src/StringStream.php
vendored
Normal file
61
libraries/vendor/web-auth/webauthn-lib/src/StringStream.php
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use CBOR\Stream;
|
||||
use function fclose;
|
||||
use function fopen;
|
||||
use function fread;
|
||||
use function fwrite;
|
||||
use function rewind;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
final class StringStream implements Stream
|
||||
{
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
private $data;
|
||||
|
||||
private readonly int $length;
|
||||
|
||||
private int $totalRead = 0;
|
||||
|
||||
public function __construct(string $data)
|
||||
{
|
||||
$this->length = mb_strlen($data, '8bit');
|
||||
$resource = fopen('php://memory', 'rb+');
|
||||
fwrite($resource, $data);
|
||||
rewind($resource);
|
||||
$this->data = $resource;
|
||||
}
|
||||
|
||||
public function read(int $length): string
|
||||
{
|
||||
if ($length <= 0) {
|
||||
return '';
|
||||
}
|
||||
$read = fread($this->data, $length);
|
||||
$bytesRead = mb_strlen($read, '8bit');
|
||||
mb_strlen($read, '8bit') === $length || throw InvalidDataException::create(null, sprintf(
|
||||
'Out of range. Expected: %d, read: %d.',
|
||||
$length,
|
||||
$bytesRead
|
||||
));
|
||||
$this->totalRead += $bytesRead;
|
||||
|
||||
return $read;
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
fclose($this->data);
|
||||
}
|
||||
|
||||
public function isEOF(): bool
|
||||
{
|
||||
return $this->totalRead === $this->length;
|
||||
}
|
||||
}
|
||||
23
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php
vendored
Normal file
23
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TokenBinding;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
final class IgnoreTokenBindingHandler implements TokenBindingHandler
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void
|
||||
{
|
||||
//Does nothing
|
||||
}
|
||||
}
|
||||
42
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/SecTokenBindingHandler.php
vendored
Normal file
42
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/SecTokenBindingHandler.php
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TokenBinding;
|
||||
|
||||
use function count;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
final class SecTokenBindingHandler implements TokenBindingHandler
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void
|
||||
{
|
||||
if ($tokenBinding->getStatus() !== TokenBinding::TOKEN_BINDING_STATUS_PRESENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request->hasHeader('Sec-Token-Binding') || throw InvalidDataException::create(
|
||||
$tokenBinding,
|
||||
'The header parameter "Sec-Token-Binding" is missing.'
|
||||
);
|
||||
$tokenBindingIds = $request->getHeader('Sec-Token-Binding');
|
||||
count($tokenBindingIds) === 1 || throw InvalidDataException::create(
|
||||
$tokenBinding,
|
||||
'The header parameter "Sec-Token-Binding" is invalid.'
|
||||
);
|
||||
$tokenBindingId = reset($tokenBindingIds);
|
||||
$tokenBindingId === $tokenBinding->getId() || throw InvalidDataException::create(
|
||||
$tokenBinding,
|
||||
'The header parameter "Sec-Token-Binding" is invalid.'
|
||||
);
|
||||
}
|
||||
}
|
||||
77
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php
vendored
Normal file
77
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TokenBinding;
|
||||
|
||||
use function array_key_exists;
|
||||
use function in_array;
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
class TokenBinding
|
||||
{
|
||||
final public const TOKEN_BINDING_STATUS_PRESENT = 'present';
|
||||
|
||||
final public const TOKEN_BINDING_STATUS_SUPPORTED = 'supported';
|
||||
|
||||
final public const TOKEN_BINDING_STATUS_NOT_SUPPORTED = 'not-supported';
|
||||
|
||||
private readonly string $status;
|
||||
|
||||
private readonly ?string $id;
|
||||
|
||||
public function __construct(string $status, ?string $id)
|
||||
{
|
||||
$status === self::TOKEN_BINDING_STATUS_PRESENT && $id === null && throw InvalidDataException::create(
|
||||
[$status, $id],
|
||||
'The member "id" is required when status is "present"'
|
||||
);
|
||||
$this->status = $status;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $json
|
||||
*/
|
||||
public static function createFormArray(array $json): self
|
||||
{
|
||||
array_key_exists('status', $json) || throw InvalidDataException::create(
|
||||
$json,
|
||||
'The member "status" is required'
|
||||
);
|
||||
$status = $json['status'];
|
||||
in_array($status, self::getSupportedStatus(), true) || throw InvalidDataException::create($json, sprintf(
|
||||
'The member "status" is invalid. Supported values are: %s',
|
||||
implode(', ', self::getSupportedStatus())
|
||||
));
|
||||
$id = array_key_exists('id', $json) ? Base64UrlSafe::decodeNoPadding($json['id']) : null;
|
||||
|
||||
return new self($status, $id);
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private static function getSupportedStatus(): array
|
||||
{
|
||||
return [
|
||||
self::TOKEN_BINDING_STATUS_PRESENT,
|
||||
self::TOKEN_BINDING_STATUS_SUPPORTED,
|
||||
self::TOKEN_BINDING_STATUS_NOT_SUPPORTED,
|
||||
];
|
||||
}
|
||||
}
|
||||
15
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php
vendored
Normal file
15
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TokenBinding;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
interface TokenBindingHandler
|
||||
{
|
||||
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void;
|
||||
}
|
||||
27
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/TokenBindingNotSupportedHandler.php
vendored
Normal file
27
libraries/vendor/web-auth/webauthn-lib/src/TokenBinding/TokenBindingNotSupportedHandler.php
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TokenBinding;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
/**
|
||||
* @deprecated Since 4.3.0 and will be removed in 5.0.0
|
||||
*/
|
||||
final class TokenBindingNotSupportedHandler implements TokenBindingHandler
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function check(TokenBinding $tokenBinding, ServerRequestInterface $request): void
|
||||
{
|
||||
$tokenBinding->getStatus() !== TokenBinding::TOKEN_BINDING_STATUS_PRESENT || throw InvalidDataException::create(
|
||||
$tokenBinding,
|
||||
'Token binding not supported.'
|
||||
);
|
||||
}
|
||||
}
|
||||
61
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php
vendored
Normal file
61
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TrustPath;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use Webauthn\Exception\InvalidTrustPathException;
|
||||
|
||||
final class CertificateTrustPath implements TrustPath
|
||||
{
|
||||
/**
|
||||
* @param string[] $certificates
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $certificates
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $certificates
|
||||
*/
|
||||
public static function create(array $certificates): self
|
||||
{
|
||||
return new self($certificates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getCertificates(): array
|
||||
{
|
||||
return $this->certificates;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createFromArray(array $data): static
|
||||
{
|
||||
array_key_exists('x5c', $data) || throw InvalidTrustPathException::create('The trust path type is invalid');
|
||||
$x5c = $data['x5c'];
|
||||
is_array($x5c) || throw InvalidTrustPathException::create(
|
||||
'The trust path type is invalid. The parameter "x5c" shall contain strings.'
|
||||
);
|
||||
|
||||
return new self($x5c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => self::class,
|
||||
'x5c' => $this->certificates,
|
||||
];
|
||||
}
|
||||
}
|
||||
47
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php
vendored
Normal file
47
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TrustPath;
|
||||
|
||||
use function array_key_exists;
|
||||
use Webauthn\Exception\InvalidTrustPathException;
|
||||
|
||||
/**
|
||||
* @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification.
|
||||
*/
|
||||
final class EcdaaKeyIdTrustPath implements TrustPath
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $ecdaaKeyId
|
||||
) {
|
||||
}
|
||||
|
||||
public function getEcdaaKeyId(): string
|
||||
{
|
||||
return $this->ecdaaKeyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => self::class,
|
||||
'ecdaaKeyId' => $this->ecdaaKeyId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createFromArray(array $data): static
|
||||
{
|
||||
array_key_exists('ecdaaKeyId', $data) || throw InvalidTrustPathException::create(
|
||||
'The trust path type is invalid'
|
||||
);
|
||||
|
||||
return new self($data['ecdaaKeyId']);
|
||||
}
|
||||
}
|
||||
31
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php
vendored
Normal file
31
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TrustPath;
|
||||
|
||||
final class EmptyTrustPath implements TrustPath
|
||||
{
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'type' => self::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createFromArray(array $data): static
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
15
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/TrustPath.php
vendored
Normal file
15
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/TrustPath.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TrustPath;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
interface TrustPath extends JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function createFromArray(array $data): static;
|
||||
}
|
||||
33
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php
vendored
Normal file
33
libraries/vendor/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\TrustPath;
|
||||
|
||||
use function array_key_exists;
|
||||
use function class_implements;
|
||||
use function in_array;
|
||||
use Webauthn\Exception\InvalidTrustPathException;
|
||||
|
||||
abstract class TrustPathLoader
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
*/
|
||||
public static function loadTrustPath(array $data): TrustPath
|
||||
{
|
||||
array_key_exists('type', $data) || throw InvalidTrustPathException::create('The trust path type is missing');
|
||||
$type = $data['type'];
|
||||
if (class_exists($type) !== true) {
|
||||
throw InvalidTrustPathException::create(
|
||||
sprintf('The trust path type "%s" is not supported', $data['type'])
|
||||
);
|
||||
}
|
||||
|
||||
$implements = class_implements($type);
|
||||
if (in_array(TrustPath::class, $implements, true)) {
|
||||
return $type::createFromArray($data);
|
||||
}
|
||||
throw InvalidTrustPathException::create(sprintf('The trust path type "%s" is not supported', $data['type']));
|
||||
}
|
||||
}
|
||||
47
libraries/vendor/web-auth/webauthn-lib/src/U2FPublicKey.php
vendored
Normal file
47
libraries/vendor/web-auth/webauthn-lib/src/U2FPublicKey.php
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn;
|
||||
|
||||
use CBOR\ByteStringObject;
|
||||
use CBOR\MapItem;
|
||||
use CBOR\MapObject;
|
||||
use CBOR\NegativeIntegerObject;
|
||||
use CBOR\UnsignedIntegerObject;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Ec2Key;
|
||||
|
||||
class U2FPublicKey
|
||||
{
|
||||
public static function isU2FKey(string $publicKey): bool
|
||||
{
|
||||
return $publicKey[0] === "\x04" && mb_strlen($publicKey, '8bit') === 65;
|
||||
}
|
||||
|
||||
public static function convertToCoseKey(string $publicKey): string
|
||||
{
|
||||
return MapObject::create([
|
||||
MapItem::create(
|
||||
UnsignedIntegerObject::create(Ec2Key::TYPE),
|
||||
UnsignedIntegerObject::create(Ec2Key::TYPE_EC2)
|
||||
),
|
||||
MapItem::create(
|
||||
UnsignedIntegerObject::create(Ec2Key::ALG),
|
||||
NegativeIntegerObject::create(Algorithms::COSE_ALGORITHM_ES256)
|
||||
),
|
||||
MapItem::create(
|
||||
NegativeIntegerObject::create(Ec2Key::DATA_CURVE),
|
||||
UnsignedIntegerObject::create(Ec2Key::CURVE_P256)
|
||||
),
|
||||
MapItem::create(
|
||||
NegativeIntegerObject::create(Ec2Key::DATA_X),
|
||||
ByteStringObject::create(mb_substr($publicKey, 1, 32, '8bit'))
|
||||
),
|
||||
MapItem::create(
|
||||
NegativeIntegerObject::create(Ec2Key::DATA_Y),
|
||||
ByteStringObject::create(mb_substr($publicKey, 33, null, '8bit'))
|
||||
),
|
||||
])->__toString();
|
||||
}
|
||||
}
|
||||
26
libraries/vendor/web-auth/webauthn-lib/src/Util/Base64.php
vendored
Normal file
26
libraries/vendor/web-auth/webauthn-lib/src/Util/Base64.php
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Util;
|
||||
|
||||
use ParagonIE\ConstantTime\Base64UrlSafe;
|
||||
use Throwable;
|
||||
use Webauthn\Exception\InvalidDataException;
|
||||
|
||||
abstract class Base64
|
||||
{
|
||||
public static function decode(string $data): string
|
||||
{
|
||||
try {
|
||||
return Base64UrlSafe::decode($data);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
|
||||
try {
|
||||
return \ParagonIE\ConstantTime\Base64::decode($data, true);
|
||||
} catch (Throwable $e) {
|
||||
throw InvalidDataException::create($data, 'Invalid data submitted', $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
libraries/vendor/web-auth/webauthn-lib/src/Util/CoseSignatureFixer.php
vendored
Normal file
53
libraries/vendor/web-auth/webauthn-lib/src/Util/CoseSignatureFixer.php
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Webauthn\Util;
|
||||
|
||||
use Cose\Algorithm\Signature\ECDSA;
|
||||
use Cose\Algorithm\Signature\ECDSA\ECSignature;
|
||||
use Cose\Algorithm\Signature\ECDSA\ES256;
|
||||
use Cose\Algorithm\Signature\ECDSA\ES256K;
|
||||
use Cose\Algorithm\Signature\ECDSA\ES384;
|
||||
use Cose\Algorithm\Signature\ECDSA\ES512;
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
|
||||
/**
|
||||
* This class fixes the signature of the ECDSA based algorithms.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @see https://www.w3.org/TR/webauthn/#signature-attestation-types
|
||||
*/
|
||||
abstract class CoseSignatureFixer
|
||||
{
|
||||
public static function fix(string $signature, Signature $algorithm): string
|
||||
{
|
||||
switch ($algorithm::identifier()) {
|
||||
case ES256K::ID:
|
||||
case ES256::ID:
|
||||
if (mb_strlen($signature, '8bit') === 64) {
|
||||
return $signature;
|
||||
}
|
||||
|
||||
return ECSignature::fromAsn1(
|
||||
$signature,
|
||||
64
|
||||
); //TODO: fix this hardcoded value by adding a dedicated method for the algorithms
|
||||
case ES384::ID:
|
||||
if (mb_strlen($signature, '8bit') === 96) {
|
||||
return $signature;
|
||||
}
|
||||
|
||||
return ECSignature::fromAsn1($signature, 96);
|
||||
case ES512::ID:
|
||||
if (mb_strlen($signature, '8bit') === 132) {
|
||||
return $signature;
|
||||
}
|
||||
|
||||
return ECSignature::fromAsn1($signature, 132);
|
||||
}
|
||||
|
||||
return $signature;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user