File "AndroidSafetyNetAttestationStatementSupport.php"
Full Path: /home/vantageo/public_html/cache/cache/cache/cache/cache/.wp-cli/wp-content/plugins/wp-phpmyadmin-extension/lib/phpMyAdmin/vendor/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php
File size: 11.72 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\AttestationStatement;
use Assert\Assertion;
use InvalidArgumentException;
use Jose\Component\Core\Algorithm as AlgorithmInterface;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm;
use Jose\Component\Signature\JWS;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
use function Safe\json_decode;
use function Safe\sprintf;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\TrustPath\CertificateTrustPath;
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport
{
/**
* @var string|null
*/
private $apiKey;
/**
* @var ClientInterface|null
*/
private $client;
/**
* @var CompactSerializer
*/
private $jwsSerializer;
/**
* @var JWSVerifier|null
*/
private $jwsVerifier;
/**
* @var RequestFactoryInterface|null
*/
private $requestFactory;
/**
* @var int
*/
private $leeway;
/**
* @var int
*/
private $maxAge;
public function __construct(?ClientInterface $client = null, ?string $apiKey = null, ?RequestFactoryInterface $requestFactory = null, ?int $leeway = null, ?int $maxAge = null)
{
if (!class_exists(Algorithm\RS256::class)) {
throw new RuntimeException('The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-signature-algorithm-rsa?');
}
if (!class_exists(JWKFactory::class)) {
throw new RuntimeException('The class Jose\Component\KeyManagement\JWKFactory is missing. Did you forget to install the package web-token/jwt-key-mgmt?');
}
if (null !== $client) {
@trigger_error('The argument "client" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $apiKey) {
@trigger_error('The argument "apiKey" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $requestFactory) {
@trigger_error('The argument "requestFactory" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "enableApiVerification".', E_USER_DEPRECATED);
}
if (null !== $maxAge) {
@trigger_error('The argument "maxAge" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "setMaxAge".', E_USER_DEPRECATED);
}
if (null !== $leeway) {
@trigger_error('The argument "leeway" is deprecated since version 3.3 and will be removed in 4.0. Please set `null` instead and use the method "setLeeway".', E_USER_DEPRECATED);
}
$this->jwsSerializer = new CompactSerializer();
$this->initJwsVerifier();
//To be removed in 4.0
$this->leeway = $leeway ?? 0;
$this->maxAge = $maxAge ?? 60000;
$this->apiKey = $apiKey;
$this->client = $client;
$this->requestFactory = $requestFactory;
}
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 mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
foreach (['ver', 'response'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
Assertion::notEmpty($attestation['attStmt'][$key], sprintf('The attestation statement value "%s" is empty.', $key));
}
$jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
$jwsHeader = $jws->getSignature(0)->getProtectedHeader();
Assertion::keyExists($jwsHeader, 'x5c', 'The response in the attestation statement must contain a "x5c" header.');
Assertion::notEmpty($jwsHeader['x5c'], 'The "x5c" parameter in the attestation statement response must contain at least one certificate.');
$certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
$attestation['attStmt']['jws'] = $jws;
return AttestationStatement::createBasic(
$this->name(),
$attestation['attStmt'],
new CertificateTrustPath($certificates)
);
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
$certificates = $trustPath->getCertificates();
$firstCertificate = current($certificates);
Assertion::string($firstCertificate, 'No certificate');
$parsedCertificate = openssl_x509_parse($firstCertificate);
Assertion::isArray($parsedCertificate, 'Invalid attestation object');
Assertion::keyExists($parsedCertificate, 'subject', 'Invalid attestation object');
Assertion::keyExists($parsedCertificate['subject'], 'CN', 'Invalid attestation object');
Assertion::eq($parsedCertificate['subject']['CN'], 'attest.android.com', '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
{
Assertion::notNull($payload, 'Invalid attestation object');
$payload = JsonConverter::decode($payload);
Assertion::isArray($payload, 'Invalid attestation object');
Assertion::keyExists($payload, 'nonce', 'Invalid attestation object. "nonce" is missing.');
Assertion::eq($payload['nonce'], base64_encode(hash('sha256', $authenticatorData->getAuthData().$clientDataJSONHash, true)), 'Invalid attestation object. Invalid nonce');
Assertion::keyExists($payload, 'ctsProfileMatch', 'Invalid attestation object. "ctsProfileMatch" is missing.');
Assertion::true($payload['ctsProfileMatch'], 'Invalid attestation object. "ctsProfileMatch" value is false.');
Assertion::keyExists($payload, 'timestampMs', 'Invalid attestation object. Timestamp is missing.');
Assertion::integer($payload['timestampMs'], 'Invalid attestation object. Timestamp shall be an integer.');
$currentTime = time() * 1000;
Assertion::lessOrEqualThan($payload['timestampMs'], $currentTime + $this->leeway, sprintf('Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
Assertion::lessOrEqualThan($currentTime - $payload['timestampMs'], $this->maxAge, 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);
Assertion::true($isValid, 'Invalid response signature');
}
private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
{
if (null === $this->client || null === $this->apiKey || null === $this->requestFactory) {
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);
Assertion::keyExists($responseBodyJson, 'isValidSignature', 'Invalid response.');
Assertion::boolean($responseBodyJson['isValidSignature'], 'Invalid response.');
Assertion::true($responseBodyJson['isValidSignature'], 'Invalid response.');
}
private function getResponseBody(ResponseInterface $response): string
{
$responseBody = '';
$response->getBody()->rewind();
while (true) {
$tmp = $response->getBody()->read(1024);
if ('' === $tmp) {
break;
}
$responseBody .= $tmp;
}
return $responseBody;
}
private function checkGoogleApiResponse(ResponseInterface $response): void
{
Assertion::eq(200, $response->getStatusCode(), 'Request did not succeeded');
Assertion::true($response->hasHeader('content-type'), 'Unrecognized response');
foreach ($response->getHeader('content-type') as $header) {
if (0 === mb_strpos($header, 'application/json')) {
return;
}
}
throw new InvalidArgumentException('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 = [
Algorithm\RS256::class, Algorithm\RS384::class, Algorithm\RS512::class,
Algorithm\PS256::class, Algorithm\PS384::class, Algorithm\PS512::class,
Algorithm\ES256::class, Algorithm\ES384::class, Algorithm\ES512::class,
Algorithm\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);
}
}