File "TPMAttestationStatementSupport.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/TPMAttestationStatementSupport.php
File size: 12.18 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 Base64Url\Base64Url;
use CBOR\Decoder;
use CBOR\MapObject;
use CBOR\OtherObject\OtherObjectManager;
use CBOR\Tag\TagObjectManager;
use Cose\Algorithms;
use Cose\Key\Ec2Key;
use Cose\Key\Key;
use Cose\Key\OkpKey;
use Cose\Key\RsaKey;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_array;
use RuntimeException;
use Safe\DateTimeImmutable;
use function Safe\sprintf;
use function Safe\unpack;
use Webauthn\AuthenticatorData;
use Webauthn\CertificateToolbox;
use Webauthn\StringStream;
use Webauthn\TrustPath\CertificateTrustPath;
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
final class TPMAttestationStatementSupport implements AttestationStatementSupport
{
public function name(): string
{
return 'tpm';
}
/**
* @param mixed[] $attestation
*/
public function load(array $attestation): AttestationStatement
{
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
Assertion::keyNotExists($attestation['attStmt'], 'ecdaaKeyId', 'ECDAA not supported');
foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) {
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
}
Assertion::eq('2.0', $attestation['attStmt']['ver'], 'Invalid attestation object');
$certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']);
Assertion::eq('8017', bin2hex($certInfo['type']), 'Invalid attestation object');
$pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']);
$pubAreaAttestedNameAlg = mb_substr($certInfo['attestedName'], 0, 2, '8bit');
$pubAreaHash = hash($this->getTPMHash($pubAreaAttestedNameAlg), $attestation['attStmt']['pubArea'], true);
$attestedName = $pubAreaAttestedNameAlg.$pubAreaHash;
Assertion::eq($attestedName, $certInfo['attestedName'], 'Invalid attested name');
$attestation['attStmt']['parsedCertInfo'] = $certInfo;
$attestation['attStmt']['parsedPubArea'] = $pubArea;
$certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']);
Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
return AttestationStatement::createAttCA(
$this->name(),
$attestation['attStmt'],
new CertificateTrustPath($certificates)
);
}
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$attToBeSigned = $authenticatorData->getAuthData().$clientDataJSONHash;
$attToBeSignedHash = hash(Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), $attToBeSigned, true);
Assertion::eq($attestationStatement->get('parsedCertInfo')['extraData'], $attToBeSignedHash, 'Invalid attestation hash');
$this->checkUniquePublicKey(
$attestationStatement->get('parsedPubArea')['unique'],
$authenticatorData->getAttestedCredentialData()->getCredentialPublicKey()
);
switch (true) {
case $attestationStatement->getTrustPath() instanceof CertificateTrustPath:
return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData);
case $attestationStatement->getTrustPath() instanceof EcdaaKeyIdTrustPath:
return $this->processWithECDAA();
default:
throw new InvalidArgumentException('Unsupported attestation statement');
}
}
private function checkUniquePublicKey(string $unique, string $cborPublicKey): void
{
$cborDecoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
$publicKey = $cborDecoder->decode(new StringStream($cborPublicKey));
Assertion::isInstanceOf($publicKey, MapObject::class, 'Invalid public key');
$key = new Key($publicKey->getNormalizedData(false));
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 new InvalidArgumentException('Invalid or unsupported key type.');
}
Assertion::eq($unique, $uniqueFromKey, 'Invalid pubArea.unique value');
}
/**
* @return mixed[]
*/
private function checkCertInfo(string $data): array
{
$certInfo = new StringStream($data);
$magic = $certInfo->read(4);
Assertion::eq('ff544347', bin2hex($magic), '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
Assertion::true($certInfo->isEOF(), '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);
$uniqueLength = unpack('n', $pubArea->read(2))[1];
$unique = $pubArea->read($uniqueLength);
Assertion::true($pubArea->isEOF(), '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
{
switch (bin2hex($type)) {
case '0001':
case '0014':
case '0016':
return [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'keyBits' => unpack('n', $stream->read(2))[1],
'exponent' => $this->getExponent($stream->read(4)),
];
case '0018':
return [
'symmetric' => $stream->read(2),
'scheme' => $stream->read(2),
'curveId' => $stream->read(2),
'kdf' => $stream->read(2),
];
default:
throw new InvalidArgumentException('Unsupported type');
}
}
private function getExponent(string $exponent): string
{
return '00000000' === bin2hex($exponent) ? Base64Url::decode('AQAB') : $exponent;
}
private function getTPMHash(string $nameAlg): string
{
switch (bin2hex($nameAlg)) {
case '0004':
return 'sha1'; //: "TPM_ALG_SHA1",
case '000b':
return 'sha256'; //: "TPM_ALG_SHA256",
case '000c':
return 'sha384'; //: "TPM_ALG_SHA384",
case '000d':
return 'sha512'; //: "TPM_ALG_SHA512",
default:
throw new InvalidArgumentException('Unsupported hash algorithm');
}
}
private function processWithCertificate(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
{
$trustPath = $attestationStatement->getTrustPath();
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, '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 1 === $result;
}
private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void
{
$parsed = openssl_x509_parse($attestnCert);
Assertion::isArray($parsed, 'Invalid certificate');
//Check version
Assertion::false(!isset($parsed['version']) || 2 !== $parsed['version'], 'Invalid certificate version');
//Check subject field is empty
Assertion::false(!isset($parsed['subject']) || !is_array($parsed['subject']) || 0 !== count($parsed['subject']), 'Invalid certificate name. The Subject should be empty');
// Check period of validity
Assertion::keyExists($parsed, 'validFrom_time_t', 'Invalid certificate start date.');
Assertion::integer($parsed['validFrom_time_t'], 'Invalid certificate start date.');
$startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']);
Assertion::true($startDate < new DateTimeImmutable(), 'Invalid certificate start date.');
Assertion::keyExists($parsed, 'validTo_time_t', 'Invalid certificate end date.');
Assertion::integer($parsed['validTo_time_t'], 'Invalid certificate end date.');
$endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']);
Assertion::true($endDate > new DateTimeImmutable(), 'Invalid certificate end date.');
//Check extensions
Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
//Check subjectAltName
Assertion::false(!isset($parsed['extensions']['subjectAltName']), 'The "subjectAltName" is missing');
//Check extendedKeyUsage
Assertion::false(!isset($parsed['extensions']['extendedKeyUsage']), 'The "subjectAltName" is missing');
Assertion::eq($parsed['extensions']['extendedKeyUsage'], '2.23.133.8.3', 'The "extendedKeyUsage" is invalid');
// id-fido-gen-ce-aaguid OID check
Assertion::false(in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && !hash_equals($authenticatorData->getAttestedCredentialData()->getAaguid()->getBytes(), $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4']), 'The value of the "aaguid" does not match with the certificate');
}
private function processWithECDAA(): bool
{
throw new RuntimeException('ECDAA not supported');
}
}