File "AuthenticatorAssertionResponseValidator.php"
Full Path: /home/vantageo/public_html/cache/cache/.wp-cli/wp-content/plugins/wp-phpmyadmin-extension/lib/phpMyAdmin/vendor/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php
File size: 11.91 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 CBOR\Decoder;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithm\Manager;
use Cose\Algorithm\Signature\Signature;
use Cose\Key\Key;
use function count;
use function in_array;
use function is_string;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use function Safe\parse_url;
use Throwable;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientInputs;
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputs;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
use Webauthn\Counter\CounterChecker;
use Webauthn\Counter\ThrowExceptionIfInvalid;
use Webauthn\TokenBinding\TokenBindingHandler;
use Webauthn\Util\CoseSignatureFixer;
class AuthenticatorAssertionResponseValidator
{
/**
* @var PublicKeyCredentialSourceRepository
*/
private $publicKeyCredentialSourceRepository;
/**
* @var Decoder
*/
private $decoder;
/**
* @var TokenBindingHandler
*/
private $tokenBindingHandler;
/**
* @var ExtensionOutputCheckerHandler
*/
private $extensionOutputCheckerHandler;
/**
* @var Manager|null
*/
private $algorithmManager;
/**
* @var CounterChecker
*/
private $counterChecker;
/**
* @var LoggerInterface|null
*/
private $logger;
public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, TokenBindingHandler $tokenBindingHandler, ExtensionOutputCheckerHandler $extensionOutputCheckerHandler, Manager $algorithmManager, ?CounterChecker $counterChecker = 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 !== $counterChecker) {
@trigger_error('The argument "counterChecker" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setCounterChecker".', E_USER_DEPRECATED);
}
$this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$this->tokenBindingHandler = $tokenBindingHandler;
$this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler;
$this->algorithmManager = $algorithmManager;
$this->counterChecker = $counterChecker ?? new ThrowExceptionIfInvalid();
$this->logger = $logger ?? new NullLogger();
}
/**
* @see https://www.w3.org/TR/webauthn/#verifying-assertion
*/
public function check(string $credentialId, AuthenticatorAssertionResponse $authenticatorAssertionResponse, PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, ServerRequestInterface $request, ?string $userHandle, array $securedRelyingPartyId = []): PublicKeyCredentialSource
{
try {
$this->logger->info('Checking the authenticator assertion response', [
'credentialId' => $credentialId,
'authenticatorAssertionResponse' => $authenticatorAssertionResponse,
'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions,
'host' => $request->getUri()->getHost(),
'userHandle' => $userHandle,
]);
/** @see 7.2.1 */
if (0 !== count($publicKeyCredentialRequestOptions->getAllowCredentials())) {
Assertion::true($this->isCredentialIdAllowed($credentialId, $publicKeyCredentialRequestOptions->getAllowCredentials()), 'The credential ID is not allowed.');
}
/** @see 7.2.2 */
$publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($credentialId);
Assertion::notNull($publicKeyCredentialSource, 'The credential ID is invalid.');
/** @see 7.2.3 */
$attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData();
$credentialUserHandle = $publicKeyCredentialSource->getUserHandle();
$responseUserHandle = $authenticatorAssertionResponse->getUserHandle();
/** @see 7.2.2 User Handle*/
if (null !== $userHandle) { //If the user was identified before the authentication ceremony was initiated,
Assertion::eq($credentialUserHandle, $userHandle, 'Invalid user handle');
if (null !== $responseUserHandle && '' !== $responseUserHandle) {
Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
}
} else {
Assertion::notEmpty($responseUserHandle, 'User handle is mandatory');
Assertion::eq($credentialUserHandle, $responseUserHandle, 'Invalid user handle');
}
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
$isU2F = U2FPublicKey::isU2FKey($credentialPublicKey);
if ($isU2F) {
$credentialPublicKey = U2FPublicKey::createCOSEKey($credentialPublicKey);
}
Assertion::notNull($credentialPublicKey, 'No public key available.');
$stream = new StringStream($credentialPublicKey);
$credentialPublicKeyStream = $this->decoder->decode($stream);
Assertion::true($stream->isEOF(), 'Invalid key. Presence of extra bytes.');
$stream->close();
/** @see 7.2.4 */
/** @see 7.2.5 */
//Nothing to do. Use of objects directly
/** @see 7.2.6 */
$C = $authenticatorAssertionResponse->getClientDataJSON();
/** @see 7.2.7 */
Assertion::eq('webauthn.get', $C->getType(), 'The client data type is not "webauthn.get".');
/** @see 7.2.8 */
Assertion::true(hash_equals($publicKeyCredentialRequestOptions->getChallenge(), $C->getChallenge()), 'Invalid challenge.');
/** @see 7.2.9 */
$rpId = $publicKeyCredentialRequestOptions->getRpId() ?? $request->getUri()->getHost();
$facetId = $this->getFacetId($rpId, $publicKeyCredentialRequestOptions->getExtensions(), $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions());
$parsedRelyingPartyId = parse_url($C->getOrigin());
Assertion::isArray($parsedRelyingPartyId, 'Invalid origin');
if (!in_array($facetId, $securedRelyingPartyId, true)) {
$scheme = $parsedRelyingPartyId['scheme'] ?? '';
Assertion::eq('https', $scheme, 'Invalid scheme. HTTPS required.');
}
$clientDataRpId = $parsedRelyingPartyId['host'] ?? '';
Assertion::notEmpty($clientDataRpId, 'Invalid origin rpId.');
$rpIdLength = mb_strlen($facetId);
Assertion::eq(mb_substr('.'.$clientDataRpId, -($rpIdLength + 1)), '.'.$facetId, 'rpId mismatch.');
/** @see 7.2.10 */
if (null !== $C->getTokenBinding()) {
$this->tokenBindingHandler->check($C->getTokenBinding(), $request);
}
$expectedRpIdHash = $isU2F ? $C->getOrigin() : $facetId;
// u2f response has full origin in rpIdHash
/** @see 7.2.11 */
$rpIdHash = hash('sha256', $expectedRpIdHash, true);
Assertion::true(hash_equals($rpIdHash, $authenticatorAssertionResponse->getAuthenticatorData()->getRpIdHash()), 'rpId hash mismatch.');
/** @see 7.2.12 */
Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserPresent(), 'User was not present');
/** @see 7.2.13 */
if (AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED === $publicKeyCredentialRequestOptions->getUserVerification()) {
Assertion::true($authenticatorAssertionResponse->getAuthenticatorData()->isUserVerified(), 'User authentication required.');
}
/** @see 7.2.14 */
$extensionsClientOutputs = $authenticatorAssertionResponse->getAuthenticatorData()->getExtensions();
if (null !== $extensionsClientOutputs) {
$this->extensionOutputCheckerHandler->check(
$publicKeyCredentialRequestOptions->getExtensions(),
$extensionsClientOutputs
);
}
/** @see 7.2.15 */
$getClientDataJSONHash = hash('sha256', $authenticatorAssertionResponse->getClientDataJSON()->getRawData(), true);
/** @see 7.2.16 */
$dataToVerify = $authenticatorAssertionResponse->getAuthenticatorData()->getAuthData().$getClientDataJSONHash;
$signature = $authenticatorAssertionResponse->getSignature();
$coseKey = new Key($credentialPublicKeyStream->getNormalizedData());
$algorithm = $this->algorithmManager->get($coseKey->alg());
Assertion::isInstanceOf($algorithm, Signature::class, 'Invalid algorithm identifier. Should refer to a signature algorithm');
$signature = CoseSignatureFixer::fix($signature, $algorithm);
Assertion::true($algorithm->verify($dataToVerify, $coseKey, $signature), 'Invalid signature.');
/** @see 7.2.17 */
$storedCounter = $publicKeyCredentialSource->getCounter();
$responseCounter = $authenticatorAssertionResponse->getAuthenticatorData()->getSignCount();
if (0 !== $responseCounter || 0 !== $storedCounter) {
$this->counterChecker->check($publicKeyCredentialSource, $responseCounter);
}
$publicKeyCredentialSource->setCounter($responseCounter);
$this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
/** @see 7.2.18 */
//All good. We can continue.
$this->logger->info('The assertion 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;
}
}
public function setLogger(LoggerInterface $logger): self
{
$this->logger = $logger;
return $this;
}
public function setCounterChecker(CounterChecker $counterChecker): self
{
$this->counterChecker = $counterChecker;
return $this;
}
/**
* @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 (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;
}
}