installed plugin WP-WebAuthn
version 1.2.8
This commit is contained in:
@ -0,0 +1,147 @@
|
||||
<?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 CBOR\Decoder;
|
||||
use CBOR\OtherObject\OtherObjectManager;
|
||||
use CBOR\Tag\TagObjectManager;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use function count;
|
||||
use FG\ASN1\ASNObject;
|
||||
use FG\ASN1\ExplicitlyTaggedObject;
|
||||
use FG\ASN1\Universal\OctetString;
|
||||
use FG\ASN1\Universal\Sequence;
|
||||
use function Safe\hex2bin;
|
||||
use function Safe\openssl_pkey_get_public;
|
||||
use function Safe\sprintf;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport
|
||||
{
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'android-key';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
|
||||
foreach (['sig', 'x5c', 'alg'] as $key) {
|
||||
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
|
||||
}
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
Assertion::greaterThan(count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
|
||||
return AttestationStatement::createBasic($attestation['fmt'], $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();
|
||||
|
||||
//Decode leaf attestation certificate
|
||||
$leaf = $certificates[0];
|
||||
$this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData);
|
||||
|
||||
$signedData = $authenticatorData->getAuthData().$clientDataJSONHash;
|
||||
$alg = $attestationStatement->get('alg');
|
||||
|
||||
return 1 === openssl_verify($signedData, $attestationStatement->get('sig'), $leaf, Algorithms::getOpensslAlgorithmFor((int) $alg));
|
||||
}
|
||||
|
||||
private function checkCertificateAndGetPublicKey(string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData): void
|
||||
{
|
||||
$resource = openssl_pkey_get_public($certificate);
|
||||
$details = openssl_pkey_get_details($resource);
|
||||
Assertion::isArray($details, 'Unable to read the certificate');
|
||||
|
||||
//Check that authData publicKey matches the public key in the attestation certificate
|
||||
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
|
||||
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
|
||||
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
|
||||
Assertion::notNull($publicKeyData, 'No attested public key found');
|
||||
$publicDataStream = new StringStream($publicKeyData);
|
||||
$coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
|
||||
Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
|
||||
$publicDataStream->close();
|
||||
$publicKey = Key::createFromData($coseKey);
|
||||
|
||||
Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
|
||||
Assertion::eq($publicKey->asPEM(), $details['key'], '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
|
||||
Assertion::isArray($certDetails, 'The certificate is not valid');
|
||||
Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
|
||||
Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
|
||||
Assertion::keyExists($certDetails['extensions'], '1.3.6.1.4.1.11129.2.1.17', '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 = ASNObject::fromBinary($extension);
|
||||
Assertion::isInstanceOf($extensionAsAsn1, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
|
||||
$objects = $extensionAsAsn1->getChildren();
|
||||
|
||||
//Check that attestationChallenge is set to the clientDataHash.
|
||||
Assertion::keyExists($objects, 4, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
|
||||
Assertion::isInstanceOf($objects[4], OctetString::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
|
||||
Assertion::eq($clientDataHash, hex2bin(($objects[4])->getContent()), 'The client data hash is not valid');
|
||||
|
||||
//Check that both teeEnforced and softwareEnforced structures don’t contain allApplications(600) tag.
|
||||
Assertion::keyExists($objects, 6, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
|
||||
$softwareEnforcedFlags = $objects[6];
|
||||
Assertion::isInstanceOf($softwareEnforcedFlags, Sequence::class, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
|
||||
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
|
||||
|
||||
Assertion::keyExists($objects, 7, 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid');
|
||||
$teeEnforcedFlags = $objects[6];
|
||||
Assertion::isInstanceOf($teeEnforcedFlags, Sequence::class, '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->getChildren() as $tag) {
|
||||
Assertion::isInstanceOf($tag, ExplicitlyTaggedObject::class, 'Invalid tag');
|
||||
/* @var ExplicitlyTaggedObject $tag */
|
||||
Assertion::notEq(600, (int) $tag->getTag(), 'Forbidden tag 600 found');
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
<?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 CBOR\Decoder;
|
||||
use CBOR\OtherObject\OtherObjectManager;
|
||||
use CBOR\Tag\TagObjectManager;
|
||||
use Cose\Key\Ec2Key;
|
||||
use Cose\Key\Key;
|
||||
use Cose\Key\RsaKey;
|
||||
use function count;
|
||||
use function Safe\openssl_pkey_get_public;
|
||||
use function Safe\sprintf;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
final class AppleAttestationStatementSupport implements AttestationStatementSupport
|
||||
{
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'apple';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
|
||||
foreach (['x5c'] as $key) {
|
||||
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
|
||||
}
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
Assertion::greaterThan(count($certificates), 0, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
|
||||
return AttestationStatement::createAnonymizationCA($attestation['fmt'], $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();
|
||||
|
||||
//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);
|
||||
Assertion::isArray($details, 'Unable to read the certificate');
|
||||
|
||||
//Check that authData publicKey matches the public key in the attestation certificate
|
||||
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
|
||||
Assertion::notNull($attestedCredentialData, 'No attested credential data found');
|
||||
$publicKeyData = $attestedCredentialData->getCredentialPublicKey();
|
||||
Assertion::notNull($publicKeyData, 'No attested public key found');
|
||||
$publicDataStream = new StringStream($publicKeyData);
|
||||
$coseKey = $this->decoder->decode($publicDataStream)->getNormalizedData(false);
|
||||
Assertion::true($publicDataStream->isEOF(), 'Invalid public key data. Presence of extra bytes.');
|
||||
$publicDataStream->close();
|
||||
$publicKey = Key::createFromData($coseKey);
|
||||
|
||||
Assertion::true(($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey), 'Unsupported key type');
|
||||
|
||||
//We check the attested key corresponds to the key in the certificate
|
||||
Assertion::eq($publicKey->asPEM(), $details['key'], 'Invalid key');
|
||||
|
||||
/*---------------------------*/
|
||||
$certDetails = openssl_x509_parse($certificate);
|
||||
|
||||
//Find Apple Extension with OID “1.2.840.113635.100.8.2” in certificate extensions
|
||||
Assertion::isArray($certDetails, 'The certificate is not valid');
|
||||
Assertion::keyExists($certDetails, 'extensions', 'The certificate has no extension');
|
||||
Assertion::isArray($certDetails['extensions'], 'The certificate has no extension');
|
||||
Assertion::keyExists($certDetails['extensions'], '1.2.840.113635.100.8.2', '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
|
||||
Assertion::eq('3024a1220420'.$nonce, bin2hex($extension), 'The client data hash is not valid');
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?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 Webauthn\AuthenticatorData;
|
||||
use Webauthn\MetadataService\MetadataStatement;
|
||||
|
||||
class AttestationObject
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $rawAttestationObject;
|
||||
/**
|
||||
* @var AttestationStatement
|
||||
*/
|
||||
private $attStmt;
|
||||
/**
|
||||
* @var AuthenticatorData
|
||||
*/
|
||||
private $authData;
|
||||
|
||||
/**
|
||||
* @var MetadataStatement|null
|
||||
*/
|
||||
private $metadataStatement;
|
||||
|
||||
public function __construct(string $rawAttestationObject, AttestationStatement $attStmt, AuthenticatorData $authData, ?MetadataStatement $metadataStatement = null)
|
||||
{
|
||||
if (null !== $metadataStatement) {
|
||||
@trigger_error('The argument "metadataStatement" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setMetadataStatement".', E_USER_DEPRECATED);
|
||||
}
|
||||
$this->rawAttestationObject = $rawAttestationObject;
|
||||
$this->attStmt = $attStmt;
|
||||
$this->authData = $authData;
|
||||
$this->metadataStatement = $metadataStatement;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
<?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 function ord;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function Safe\sprintf;
|
||||
use function Safe\unpack;
|
||||
use Throwable;
|
||||
use Webauthn\AttestedCredentialData;
|
||||
use Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\MetadataService\MetadataStatementRepository;
|
||||
use Webauthn\StringStream;
|
||||
|
||||
class AttestationObjectLoader
|
||||
{
|
||||
private const FLAG_AT = 0b01000000;
|
||||
private const FLAG_ED = 0b10000000;
|
||||
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* @var AttestationStatementSupportManager
|
||||
*/
|
||||
private $attestationStatementSupportManager;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface|null
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
public function __construct(AttestationStatementSupportManager $attestationStatementSupportManager, ?MetadataStatementRepository $metadataStatementRepository = null, ?LoggerInterface $logger = null)
|
||||
{
|
||||
if (null !== $metadataStatementRepository) {
|
||||
@trigger_error('The argument "metadataStatementRepository" is deprecated since version 3.2 and will be removed in 4.0. Please set `null` instead.', E_USER_DEPRECATED);
|
||||
}
|
||||
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" instead.', E_USER_DEPRECATED);
|
||||
}
|
||||
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
|
||||
$this->attestationStatementSupportManager = $attestationStatementSupportManager;
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
}
|
||||
|
||||
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 = Base64Url::decode($data);
|
||||
$stream = new StringStream($decodedData);
|
||||
$parsed = $this->decoder->decode($stream);
|
||||
|
||||
$this->logger->info('Loading the Attestation Statement');
|
||||
$attestationObject = $parsed->getNormalizedData();
|
||||
Assertion::true($stream->isEOF(), 'Invalid attestation object. Presence of extra bytes.');
|
||||
$stream->close();
|
||||
Assertion::isArray($attestationObject, 'Invalid attestation object');
|
||||
Assertion::keyExists($attestationObject, 'authData', 'Invalid attestation object');
|
||||
Assertion::keyExists($attestationObject, 'fmt', 'Invalid attestation object');
|
||||
Assertion::keyExists($attestationObject, 'attStmt', '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)[1];
|
||||
$this->logger->debug(sprintf('Signature counter: %d', $signCount));
|
||||
|
||||
$attestedCredentialData = null;
|
||||
if (0 !== (ord($flags) & self::FLAG_AT)) {
|
||||
$this->logger->info('Attested Credential Data is present');
|
||||
$aaguid = Uuid::fromBytes($authDataStream->read(16));
|
||||
$credentialLength = $authDataStream->read(2);
|
||||
$credentialLength = unpack('n', $credentialLength)[1];
|
||||
$credentialId = $authDataStream->read($credentialLength);
|
||||
$credentialPublicKey = $this->decoder->decode($authDataStream);
|
||||
Assertion::isInstanceOf($credentialPublicKey, MapObject::class, '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]);
|
||||
}
|
||||
Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.');
|
||||
$authDataStream->close();
|
||||
|
||||
$authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
|
||||
$attestationObject = new AttestationObject($data, $attestationStatement, $authenticatorData);
|
||||
$this->logger->info('Attestation Object loaded');
|
||||
$this->logger->debug('Attestation Object', ['ed' => $attestationObject]);
|
||||
|
||||
return $attestationObject;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('An error occurred', [
|
||||
'exception' => $throwable,
|
||||
]);
|
||||
throw $throwable;
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogger(LoggerInterface $logger): self
|
||||
{
|
||||
$this->logger = $logger;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
<?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 function array_key_exists;
|
||||
use Assert\Assertion;
|
||||
use JsonSerializable;
|
||||
use function Safe\sprintf;
|
||||
use Webauthn\TrustPath\TrustPath;
|
||||
use Webauthn\TrustPath\TrustPathLoader;
|
||||
|
||||
class AttestationStatement implements JsonSerializable
|
||||
{
|
||||
public const TYPE_NONE = 'none';
|
||||
public const TYPE_BASIC = 'basic';
|
||||
public const TYPE_SELF = 'self';
|
||||
public const TYPE_ATTCA = 'attca';
|
||||
public const TYPE_ECDAA = 'ecdaa';
|
||||
public const TYPE_ANONCA = 'anonca';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $fmt;
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private $attStmt;
|
||||
|
||||
/**
|
||||
* @var TrustPath
|
||||
*/
|
||||
private $trustPath;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @param mixed[] $attStmt
|
||||
*/
|
||||
public function __construct(string $fmt, array $attStmt, string $type, TrustPath $trustPath)
|
||||
{
|
||||
$this->fmt = $fmt;
|
||||
$this->attStmt = $attStmt;
|
||||
$this->type = $type;
|
||||
$this->trustPath = $trustPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attStmt
|
||||
*/
|
||||
public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_NONE, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attStmt
|
||||
*/
|
||||
public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_BASIC, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attStmt
|
||||
*/
|
||||
public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_SELF, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attStmt
|
||||
*/
|
||||
public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_ATTCA, $trustPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attStmt
|
||||
*/
|
||||
public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self
|
||||
{
|
||||
return new self($fmt, $attStmt, self::TYPE_ECDAA, $trustPath);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
Assertion::true($this->has($key), 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) {
|
||||
Assertion::keyExists($data, $key, 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,
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?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 Webauthn\AuthenticatorData;
|
||||
|
||||
interface AttestationStatementSupport
|
||||
{
|
||||
public function name(): string;
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement;
|
||||
|
||||
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool;
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?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 function array_key_exists;
|
||||
use Assert\Assertion;
|
||||
use function Safe\sprintf;
|
||||
|
||||
class AttestationStatementSupportManager
|
||||
{
|
||||
/**
|
||||
* @var AttestationStatementSupport[]
|
||||
*/
|
||||
private $attestationStatementSupports = [];
|
||||
|
||||
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
|
||||
{
|
||||
Assertion::true($this->has($name), sprintf('The attestation statement format "%s" is not supported.', $name));
|
||||
|
||||
return $this->attestationStatementSupports[$name];
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
<?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 CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use CBOR\OtherObject\OtherObjectManager;
|
||||
use CBOR\Tag\TagObjectManager;
|
||||
use Cose\Key\Ec2Key;
|
||||
use InvalidArgumentException;
|
||||
use function Safe\openssl_pkey_get_public;
|
||||
use function Safe\sprintf;
|
||||
use Throwable;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
|
||||
final class FidoU2FAttestationStatementSupport implements AttestationStatementSupport
|
||||
{
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'fido-u2f';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
Assertion::keyExists($attestation, 'attStmt', 'Invalid attestation object');
|
||||
foreach (['sig', 'x5c'] as $key) {
|
||||
Assertion::keyExists($attestation['attStmt'], $key, sprintf('The attestation statement value "%s" is missing.', $key));
|
||||
}
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
|
||||
Assertion::count($certificates, 1, 'The attestation statement value "x5c" must be a list with one certificate.');
|
||||
Assertion::allString($certificates, 'The attestation statement value "x5c" must be a list with one certificate.');
|
||||
|
||||
reset($certificates);
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
$this->checkCertificate($certificates[0]);
|
||||
|
||||
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
|
||||
}
|
||||
|
||||
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
|
||||
{
|
||||
Assertion::eq(
|
||||
$authenticatorData->getAttestedCredentialData()->getAaguid()->toString(),
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"'
|
||||
);
|
||||
$trustPath = $attestationStatement->getTrustPath();
|
||||
Assertion::isInstanceOf($trustPath, CertificateTrustPath::class, 'Invalid trust path');
|
||||
$dataToVerify = "\0";
|
||||
$dataToVerify .= $authenticatorData->getRpIdHash();
|
||||
$dataToVerify .= $clientDataJSONHash;
|
||||
$dataToVerify .= $authenticatorData->getAttestedCredentialData()->getCredentialId();
|
||||
$dataToVerify .= $this->extractPublicKey($authenticatorData->getAttestedCredentialData()->getCredentialPublicKey());
|
||||
|
||||
return 1 === openssl_verify($dataToVerify, $attestationStatement->get('sig'), $trustPath->getCertificates()[0], OPENSSL_ALGO_SHA256);
|
||||
}
|
||||
|
||||
private function extractPublicKey(?string $publicKey): string
|
||||
{
|
||||
Assertion::notNull($publicKey, 'The attested credential data does not contain a valid public key.');
|
||||
|
||||
$publicKeyStream = new StringStream($publicKey);
|
||||
$coseKey = $this->decoder->decode($publicKeyStream);
|
||||
Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.');
|
||||
$publicKeyStream->close();
|
||||
Assertion::isInstanceOf($coseKey, MapObject::class, 'The attested credential data does not contain a valid public key.');
|
||||
|
||||
$coseKey = $coseKey->getNormalizedData();
|
||||
$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 new InvalidArgumentException('Invalid certificate or certificate chain', 0, $throwable);
|
||||
}
|
||||
Assertion::isArray($details, 'Invalid certificate or certificate chain');
|
||||
Assertion::keyExists($details, 'ec', 'Invalid certificate or certificate chain');
|
||||
Assertion::keyExists($details['ec'], 'curve_name', 'Invalid certificate or certificate chain');
|
||||
Assertion::eq($details['ec']['curve_name'], 'prime256v1', 'Invalid certificate or certificate chain');
|
||||
Assertion::keyExists($details['ec'], 'curve_oid', 'Invalid certificate or certificate chain');
|
||||
Assertion::eq($details['ec']['curve_oid'], '1.2.840.10045.3.1.7', 'Invalid certificate or certificate chain');
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<?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 function count;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
|
||||
final class NoneAttestationStatementSupport implements AttestationStatementSupport
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
Assertion::noContent($attestation['attStmt'], 'Invalid attestation object');
|
||||
|
||||
return AttestationStatement::createNone($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath());
|
||||
}
|
||||
|
||||
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
|
||||
{
|
||||
return 0 === count($attestationStatement->getAttStmt());
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
<?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 function array_key_exists;
|
||||
use Assert\Assertion;
|
||||
use CBOR\Decoder;
|
||||
use CBOR\MapObject;
|
||||
use CBOR\OtherObject\OtherObjectManager;
|
||||
use CBOR\Tag\TagObjectManager;
|
||||
use Cose\Algorithm\Manager;
|
||||
use Cose\Algorithm\Signature\Signature;
|
||||
use Cose\Algorithms;
|
||||
use Cose\Key\Key;
|
||||
use function in_array;
|
||||
use InvalidArgumentException;
|
||||
use function is_array;
|
||||
use RuntimeException;
|
||||
use Webauthn\AuthenticatorData;
|
||||
use Webauthn\CertificateToolbox;
|
||||
use Webauthn\StringStream;
|
||||
use Webauthn\TrustPath\CertificateTrustPath;
|
||||
use Webauthn\TrustPath\EcdaaKeyIdTrustPath;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
use Webauthn\Util\CoseSignatureFixer;
|
||||
|
||||
final class PackedAttestationStatementSupport implements AttestationStatementSupport
|
||||
{
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* @var Manager
|
||||
*/
|
||||
private $algorithmManager;
|
||||
|
||||
public function __construct(Manager $algorithmManager)
|
||||
{
|
||||
$this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
|
||||
$this->algorithmManager = $algorithmManager;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'packed';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
public function load(array $attestation): AttestationStatement
|
||||
{
|
||||
Assertion::keyExists($attestation['attStmt'], 'sig', 'The attestation statement value "sig" is missing.');
|
||||
Assertion::keyExists($attestation['attStmt'], 'alg', 'The attestation statement value "alg" is missing.');
|
||||
Assertion::string($attestation['attStmt']['sig'], 'The attestation statement value "sig" is missing.');
|
||||
switch (true) {
|
||||
case array_key_exists('x5c', $attestation['attStmt']):
|
||||
return $this->loadBasicType($attestation);
|
||||
case array_key_exists('ecdaaKeyId', $attestation['attStmt']):
|
||||
return $this->loadEcdaaType($attestation['attStmt']);
|
||||
default:
|
||||
return $this->loadEmptyType($attestation);
|
||||
}
|
||||
}
|
||||
|
||||
public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
|
||||
{
|
||||
$trustPath = $attestationStatement->getTrustPath();
|
||||
switch (true) {
|
||||
case $trustPath instanceof CertificateTrustPath:
|
||||
return $this->processWithCertificate($clientDataJSONHash, $attestationStatement, $authenticatorData, $trustPath);
|
||||
case $trustPath instanceof EcdaaKeyIdTrustPath:
|
||||
return $this->processWithECDAA();
|
||||
case $trustPath instanceof EmptyTrustPath:
|
||||
return $this->processWithSelfAttestation($clientDataJSONHash, $attestationStatement, $authenticatorData);
|
||||
default:
|
||||
throw new InvalidArgumentException('Unsupported attestation statement');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
private function loadBasicType(array $attestation): AttestationStatement
|
||||
{
|
||||
$certificates = $attestation['attStmt']['x5c'];
|
||||
Assertion::isArray($certificates, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
Assertion::minCount($certificates, 1, 'The attestation statement value "x5c" must be a list with at least one certificate.');
|
||||
$certificates = CertificateToolbox::convertAllDERToPEM($certificates);
|
||||
|
||||
return AttestationStatement::createBasic($attestation['fmt'], $attestation['attStmt'], new CertificateTrustPath($certificates));
|
||||
}
|
||||
|
||||
private function loadEcdaaType(array $attestation): AttestationStatement
|
||||
{
|
||||
$ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId'];
|
||||
Assertion::string($ecdaaKeyId, 'The attestation statement value "ecdaaKeyId" is invalid.');
|
||||
|
||||
return AttestationStatement::createEcdaa($attestation['fmt'], $attestation['attStmt'], new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $attestation
|
||||
*/
|
||||
private function loadEmptyType(array $attestation): AttestationStatement
|
||||
{
|
||||
return AttestationStatement::createSelf($attestation['fmt'], $attestation['attStmt'], new EmptyTrustPath());
|
||||
}
|
||||
|
||||
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
|
||||
Assertion::false(!isset($parsed['name']) || false === mb_strpos($parsed['name'], '/OU=Authenticator Attestation'), 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"');
|
||||
|
||||
//Check extensions
|
||||
Assertion::false(!isset($parsed['extensions']) || !is_array($parsed['extensions']), 'Certificate extensions are missing');
|
||||
|
||||
//Check certificate is not a CA cert
|
||||
Assertion::false(!isset($parsed['extensions']['basicConstraints']) || 'CA:FALSE' !== $parsed['extensions']['basicConstraints'], 'The Basic Constraints extension must have the CA component set to false');
|
||||
|
||||
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
|
||||
Assertion::notNull($attestedCredentialData, 'No attested credential available');
|
||||
|
||||
// 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($attestedCredentialData->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 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 1 === $result;
|
||||
}
|
||||
|
||||
private function processWithECDAA(): bool
|
||||
{
|
||||
throw new RuntimeException('ECDAA not supported');
|
||||
}
|
||||
|
||||
private function processWithSelfAttestation(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
|
||||
{
|
||||
$attestedCredentialData = $authenticatorData->getAttestedCredentialData();
|
||||
Assertion::notNull($attestedCredentialData, 'No attested credential available');
|
||||
$credentialPublicKey = $attestedCredentialData->getCredentialPublicKey();
|
||||
Assertion::notNull($credentialPublicKey, 'No credential public key available');
|
||||
$publicKeyStream = new StringStream($credentialPublicKey);
|
||||
$publicKey = $this->decoder->decode($publicKeyStream);
|
||||
Assertion::true($publicKeyStream->isEOF(), 'Invalid public key. Presence of extra bytes.');
|
||||
$publicKeyStream->close();
|
||||
Assertion::isInstanceOf($publicKey, MapObject::class, 'The attested credential data does not contain a valid public key.');
|
||||
$publicKey = $publicKey->getNormalizedData(false);
|
||||
$publicKey = new Key($publicKey);
|
||||
Assertion::eq($publicKey->alg(), (int) $attestationStatement->get('alg'), '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 new RuntimeException('Invalid algorithm');
|
||||
}
|
||||
$signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm);
|
||||
|
||||
return $algorithm->verify($dataToVerify, $publicKey, $signature);
|
||||
}
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user