File "AuthenticatorAttestationResponseValidator.php"
Full Path: /home/vantageo/public_html/cache/cache/cache/cache/cache/cache/.wp-cli/wp-content/plugins/wp-phpmyadmin-extension/lib/phpMyAdmin/vendor/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php
File size: 17.38 KB
MIME-type: text/x-php
Charset: utf-8
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2021 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Webauthn;
use Assert\Assertion;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_string;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Ramsey\Uuid\Uuid;
use function Safe\parse_url;
use function Safe\sprintf;
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\CertificateChainChecker\CertificateChainChecker;
use Webauthn\MetadataService\MetadataStatement;
use Webauthn\MetadataService\MetadataStatementRepository;
use Webauthn\MetadataService\StatusReport;
use Webauthn\TokenBinding\TokenBindingHandler;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EmptyTrustPath;
class AuthenticatorAttestationResponseValidator
{
/**
* @var AttestationStatementSupportManager
*/
private $attestationStatementSupportManager;
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSource;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var MetadataStatementRepository|null
*/
private $metadataStatementRepository;
/**
* @var CertificateChainChecker|null
*/
private $certificateChainChecker;
public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, PublicKeyCredentialSourceRepository $publicKeyCredentialSource, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
{
if (null !== $logger) {
@trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', E_USER_DEPRECATED);
}
if (null !== $metadataStatementRepository) {
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatementRepository".', E_USER_DEPRECATED);
}
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
$this->publicKeyCredentialSource = $publicKeyCredentialSource;
$this->tokenBindingHandler = $tokenBindingHandler;
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
$this->metadataStatementRepository = $metadataStatementRepository;
$this->logger = $logger ?? new NullLogger();
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function setCertificateChainChecker(CertificateChainChecker $certificateChainChecker): self
{
$this->certificateChainChecker = $certificateChainChecker;
return $this;
}
public function setMetadataStatementRepository(MetadataStatementRepository $metadataStatementRepository): self
{
$this->metadataStatementRepository = $metadataStatementRepository;
return $this;
}
/**
* @see https://www.w3.org/TR/webauthn/#registering-a-new-credential
*/
public function check(AuthenticatorAttestationResponse $authenticatorAttestationResponse, PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, ServerRequestInterface $request, array $securedRelyingPartyId = []): PublicKeyCredentialSource
{
try {
$this->logger->info('Checking the authenticator attestation response', [
'authenticatorAttestationResponse' => $authenticatorAttestationResponse,
'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions,
'host' => $request->getUri()->getHost(),
]);
/** @see 7.1.1 */
//Nothing to do
/** @see 7.1.2 */
$C = $authenticatorAttestationResponse->getClientDataJSON();
/** @see 7.1.3 */
Assertion::eq('webauthn.create', $C->getType(), 'The client data type is not "webauthn.create".');
/** @see 7.1.4 */
Assertion::true(hash_equals($publicKeyCredentialCreationOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
/** @see 7.1.5 */
$rpId = $publicKeyCredentialCreationOptions->getRp()->getId() ?? $request->getUri()->getHost();
$facetId = $this->getFacetId($rpId, $publicKeyCredentialCreationOptions->getExtensions(), $authenticatorAttestationResponse->getAttestationObject()->getAuthData()->getExtensions());
$parsedRelyingPartyId = parse_url($C->getOrigin());
Assertion::isArray($parsedRelyingPartyId, sprintf('The origin URI "%s" is not valid', $C->getOrigin()));
Assertion::keyExists($parsedRelyingPartyId, 'scheme', 'Invalid origin rpId.');
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
if (!in_array($facetId, $securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
}
/** @see 7.1.6 */
if (null !== $C->getTokenBinding()) {
$this->tokenBindingHandler->check($C->getTokenBinding(), $request);
}
/** @see 7.1.7 */
$clientDataJSONHash = hash('sha256', $authenticatorAttestationResponse->getClientDataJSON()->getRawData(), true);
/** @see 7.1.8 */
$attestationObject = $authenticatorAttestationResponse->getAttestationObject();
/** @see 7.1.9 */
$rpIdHash = hash('sha256', $facetId, true);
Assertion::true(hash_equals($rpIdHash, $attestationObject->getAuthData()->getRpIdHash()), 'rpId hash mismatch.');
/** @see 7.1.10 */
Assertion::true($attestationObject->getAuthData()->isUserPresent(), 'User was not present');
/** @see 7.1.11 */
if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialCreationOptions->getAuthenticatorSelection()->getUserVerification()) {
Assertion::true($attestationObject->getAuthData()->isUserVerified(), 'User authentication required.');
}
/** @see 7.1.12 */
$extensionsClientOutputs = $attestationObject->getAuthData()->getExtensions();
if (null !== $extensionsClientOutputs) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialCreationOptions->getExtensions(),
$extensionsClientOutputs
);
}
/** @see 7.1.13 */
$this->checkMetadataStatement($publicKeyCredentialCreationOptions, $attestationObject);
$fmt = $attestationObject->getAttStmt()->getFmt();
Assertion::true($this->attestationStatementSupportManager->has($fmt), 'Unsupported attestation statement format.');
/** @see 7.1.14 */
$attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt);
Assertion::true($attestationStatementSupport->isValid($clientDataJSONHash, $attestationObject->getAttStmt(), $attestationObject->getAuthData()), 'Invalid attestation statement.');
/** @see 7.1.15 */
/** @see 7.1.16 */
/** @see 7.1.17 */
Assertion::true($attestationObject->getAuthData()->hasAttestedCredentialData(), 'There is no attested credential data.');
$attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'There is no attested credential data.');
$credentialId = $attestedCredentialData->getCredentialId();
Assertion::null($this->publicKeyCredentialSource->findOneByCredentialId($credentialId), 'The credential ID already exists.');
/** @see 7.1.18 */
/** @see 7.1.19 */
$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]);
return $publicKeyCredentialSource;
} catch (Throwable $throwable) {
$this->logger->error('An error occurred', [
'exception' => $throwable,
]);
throw $throwable;
}
}
private function checkCertificateChain(AttestationStatement $attestationStatement, ?MetadataStatement $metadataStatement): void
{
$trustPath = $attestationStatement->getTrustPath();
if (!$trustPath instanceof CertificateTrustPath) {
return;
}
$authenticatorCertificates = $trustPath->getCertificates();
if (null === $metadataStatement) {
// @phpstan-ignore-next-line
null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, [], null);
return;
}
$metadataStatementCertificates = $metadataStatement->getAttestationRootCertificates();
$rootStatementCertificates = $metadataStatement->getRootCertificates();
foreach ($metadataStatementCertificates as $key => $metadataStatementCertificate) {
$metadataStatementCertificates[$key] = CertificateToolbox::fixPEMStructure($metadataStatementCertificate);
}
$trustedCertificates = array_merge(
$metadataStatementCertificates,
$rootStatementCertificates
);
// @phpstan-ignore-next-line
null === $this->certificateChainChecker ? CertificateToolbox::checkChain($authenticatorCertificates, $trustedCertificates) : $this->certificateChainChecker->check($authenticatorCertificates, $trustedCertificates);
}
private function checkMetadataStatement(PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, AttestationObject $attestationObject): void
{
$attestationStatement = $attestationObject->getAttStmt();
$attestedCredentialData = $attestationObject->getAuthData()->getAttestedCredentialData();
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
$aaguid = $attestedCredentialData->getAaguid()->toString();
if (PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE === $publicKeyCredentialCreationOptions->getAttestation()) {
$this->logger->debug('No attestation is asked.');
//No attestation is asked. We shall ensure that the data is anonymous.
if (
'00000000-0000-0000-0000-000000000000' === $aaguid
&& (AttestationStatement::TYPE_NONE === $attestationStatement->getType() || AttestationStatement::TYPE_SELF === $attestationStatement->getType())) {
$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 (AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
$this->logger->debug('No attestation returned.');
//No attestation is returned. We shall ensure that the AAGUID is a null one.
if ('00000000-0000-0000-0000-000000000000' !== $aaguid) {
$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;
}
//The MDS Repository is mandatory here
Assertion::notNull($this->metadataStatementRepository, 'The Metadata Statement Repository is mandatory when requesting attestation objects.');
$metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid);
// We check the last status report
$this->checkStatusReport(null === $metadataStatement ? [] : $metadataStatement->getStatusReports());
// We check the certificate chain (if any)
$this->checkCertificateChain($attestationStatement, $metadataStatement);
// If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
// => nothing to check
if ('00000000-0000-0000-0000-000000000000' === $aaguid || AttestationStatement::TYPE_NONE === $attestationStatement->getType()) {
return;
}
// At this point, the Metadata Statement is mandatory
Assertion::notNull($metadataStatement, sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid));
// Check Attestation Type is allowed
if (0 !== count($metadataStatement->getAttestationTypes())) {
$type = $this->getAttestationType($attestationStatement);
Assertion::inArray($type, $metadataStatement->getAttestationTypes(), 'Invalid attestation statement. The attestation type is not allowed for this authenticator');
}
}
/**
* @param StatusReport[] $statusReports
*/
private function checkStatusReport(array $statusReports): void
{
if (0 !== count($statusReports)) {
$lastStatusReport = end($statusReports);
if ($lastStatusReport->isCompromised()) {
throw new LogicException('The authenticator is compromised and cannot be used');
}
}
}
private function createPublicKeyCredentialSource(string $credentialId, AttestedCredentialData $attestedCredentialData, AttestationObject $attestationObject, string $userHandle): PublicKeyCredentialSource
{
return new PublicKeyCredentialSource(
$credentialId,
PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY,
[],
$attestationObject->getAttStmt()->getType(),
$attestationObject->getAttStmt()->getTrustPath(),
$attestedCredentialData->getAaguid(),
$attestedCredentialData->getCredentialPublicKey(),
$userHandle,
$attestationObject->getAuthData()->getSignCount()
);
}
private function getAttestationType(AttestationStatement $attestationStatement): int
{
switch ($attestationStatement->getType()) {
case AttestationStatement::TYPE_BASIC:
return MetadataStatement::ATTESTATION_BASIC_FULL;
case AttestationStatement::TYPE_SELF:
return MetadataStatement::ATTESTATION_BASIC_SURROGATE;
case AttestationStatement::TYPE_ATTCA:
return MetadataStatement::ATTESTATION_ATTCA;
case AttestationStatement::TYPE_ECDAA:
return MetadataStatement::ATTESTATION_ECDAA;
default:
throw new InvalidArgumentException('Invalid attestation type');
}
}
private function getFacetId(string $rpId, AuthenticationExtensionsClientInputs $authenticationExtensionsClientInputs, ?AuthenticationExtensionsClientOutputs $authenticationExtensionsClientOutputs): string
{
if (null === $authenticationExtensionsClientOutputs || !$authenticationExtensionsClientInputs->has('appid') || !$authenticationExtensionsClientOutputs->has('appid')) {
return $rpId;
}
$appId = $authenticationExtensionsClientInputs->get('appid')->value();
$wasUsed = $authenticationExtensionsClientOutputs->get('appid')->value();
if (!is_string($appId) || true !== $wasUsed) {
return $rpId;
}
return $appId;
}
}