From 46bcb7be2ebdcca482d15fc6ac63545c75c74090 Mon Sep 17 00:00:00 2001 From: zll600 <3400692417@qq.com> Date: Fri, 21 Nov 2025 13:26:58 +0800 Subject: [PATCH] refactor: use spomky-labs/pki-framework to replace native php openssl functions for attestation statements --- .../AndroidKeyAttestationStatementSupport.php | 87 ++++++++------ .../AppleAttestationStatementSupport.php | 73 ++++++----- .../PackedAttestationStatementSupport.php | 78 +++++++----- .../TPMAttestationStatementSupport.php | 113 +++++++++--------- 4 files changed, 200 insertions(+), 151 deletions(-) diff --git a/src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php b/src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php index 00242354..3b3b1b6d 100644 --- a/src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php +++ b/src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php @@ -14,6 +14,9 @@ use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence; use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString; use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging; +use SpomkyLabs\Pki\CryptoEncoding\PEM; +use SpomkyLabs\Pki\X509\Certificate\Certificate; +use SpomkyLabs\Pki\X509\Certificate\Extension\UnknownExtension; use Webauthn\AuthenticatorData; use Webauthn\Event\AttestationStatementLoaded; use Webauthn\Event\CanDispatchEvents; @@ -26,13 +29,19 @@ use Webauthn\TrustPath\CertificateTrustPath; use function array_key_exists; use function count; -use function is_array; -use function openssl_pkey_get_public; use function openssl_verify; use function sprintf; final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents { + private const OID_ANDROID = '1.3.6.1.4.1.11129.2.1.17'; + + /** + * Tag 600 (allApplications) + * @see https://source.android.com/docs/security/features/keystore/attestation#version-1 + */ + private const ANDROID_TAG_ALL_APPLICATIONS = 600; + private readonly Decoder $decoder; private EventDispatcherInterface $dispatcher; @@ -117,17 +126,14 @@ public function isValid( ) === 1; } + /** + * @see https://www.w3.org/TR/webauthn-3/#sctn-android-key-attestation + */ private function checkCertificate( string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData ): void { - $resource = openssl_pkey_get_public($certificate); - $details = openssl_pkey_get_details($resource); - is_array($details) || throw AttestationStatementVerificationException::create( - 'Unable to read the certificate' - ); - //Check that authData publicKey matches the public key in the attestation certificate $attestedCredentialData = $authenticatorData->attestedCredentialData; $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( @@ -148,45 +154,46 @@ private function checkCertificate( ); $publicDataStream->close(); $publicKey = Key::createFromData($coseKey->normalize()); - ($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create( 'Unsupported key type' ); - $publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create( + + /*---------------------------*/ + /** + * @see https://w3c.github.io/webauthn/#sctn-key-attstn-cert-requirements + * @see https://source.android.com/docs/security/features/keystore/attestation#attestation-certificate + */ + $cert = Certificate::fromPEM(PEM::fromString($certificate)); + //We check the attested key corresponds to the key in the certificate + PEM::fromString( + $publicKey->asPEM() + )->string() === $cert->tbsCertificate()->subjectPublicKeyInfo()->toPEM()->string() || throw AttestationStatementVerificationException::create( 'Invalid key' ); - /*---------------------------*/ - $certDetails = openssl_x509_parse($certificate); + $extensions = $cert->tbsCertificate() + ->extensions(); - //Find Android KeyStore Extension with OID "1.3.6.1.4.1.11129.2.1.17" in certificate extensions - is_array( - $certDetails - ) || throw AttestationStatementVerificationException::create('The certificate is not valid'); - array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create( - 'The certificate has no extension' + //Find Android KeyStore Extension with OID self::OID_ANDROID in certificate extensions + $extensions->has(self::OID_ANDROID) || throw AttestationStatementVerificationException::create( + 'The certificate extension "' . self::OID_ANDROID . '" is missing' ); - is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create( - 'The certificate has no extension' - ); - array_key_exists( - '1.3.6.1.4.1.11129.2.1.17', - $certDetails['extensions'] - ) || throw AttestationStatementVerificationException::create( - '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 = Sequence::fromDER($extension); - $extensionAsAsn1->has(4); + /** @var UnknownExtension $androidExtension */ + $androidExtension = $extensions->get(self::OID_ANDROID); + /** + * Parse the Android extension value structure + * @see https://source.android.com/docs/security/features/keystore/attestation#attestation-extension + */ + $extensionAsAsn1 = Sequence::fromDER($androidExtension->extensionValue()); //Check that attestationChallenge is set to the clientDataHash. $extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + 'The attestationChallenge field is missing' ); $ext = $extensionAsAsn1->at(4) ->asElement(); $ext instanceof OctetString || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + 'The attestationChallenge field must be an OctetString' ); $clientDataHash === $ext->string() || throw AttestationStatementVerificationException::create( 'The client data hash is not valid' @@ -194,23 +201,23 @@ private function checkCertificate( //Check that both teeEnforced and softwareEnforced structures don't contain allApplications(600) tag. $extensionAsAsn1->has(6) || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + 'The softwareEnforced field is missing' ); $softwareEnforcedFlags = $extensionAsAsn1->at(6) ->asElement(); $softwareEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + 'The softwareEnforced field must be a Sequence' ); $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags); $extensionAsAsn1->has(7) || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + 'The teeEnforced field is missing' ); $teeEnforcedFlags = $extensionAsAsn1->at(7) ->asElement(); $teeEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + 'The teeEnforced field must be a Sequence' ); $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags); } @@ -218,11 +225,13 @@ private function checkCertificate( private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void { foreach ($sequence->elements() as $tag) { - $tag->asElement() instanceof ExplicitTagging || throw AttestationStatementVerificationException::create( + $element = $tag->asElement(); + $element instanceof ExplicitTagging || throw AttestationStatementVerificationException::create( 'Invalid tag' ); - $tag->asElement() - ->tag() !== 600 || throw AttestationStatementVerificationException::create('Forbidden tag 600 found'); + $element->tag() !== self::ANDROID_TAG_ALL_APPLICATIONS || throw AttestationStatementVerificationException::create( + 'The allApplications tag (' . self::ANDROID_TAG_ALL_APPLICATIONS . ') is forbidden - key must be bound to specific application' + ); } } } diff --git a/src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php b/src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php index e57c3383..beb868eb 100644 --- a/src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php +++ b/src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php @@ -10,6 +10,12 @@ use Cose\Key\Key; use Cose\Key\RsaKey; use Psr\EventDispatcher\EventDispatcherInterface; +use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence; +use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString; +use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging; +use SpomkyLabs\Pki\CryptoEncoding\PEM; +use SpomkyLabs\Pki\X509\Certificate\Certificate; +use SpomkyLabs\Pki\X509\Certificate\Extension\UnknownExtension; use Webauthn\AuthenticatorData; use Webauthn\Event\AttestationStatementLoaded; use Webauthn\Event\CanDispatchEvents; @@ -22,11 +28,11 @@ use Webauthn\TrustPath\CertificateTrustPath; use function array_key_exists; use function count; -use function is_array; -use function openssl_pkey_get_public; final class AppleAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents { + private const OID_APPLE = '1.2.840.113635.100.8.2'; + private readonly Decoder $decoder; private EventDispatcherInterface $dispatcher; @@ -105,17 +111,14 @@ public function isValid( return true; } + /** + * @see https://www.w3.org/TR/webauthn-3/#sctn-apple-anonymous-attestation + */ private function checkCertificateAndGetPublicKey( string $certificate, string $clientDataHash, AuthenticatorData $authenticatorData ): void { - $resource = openssl_pkey_get_public($certificate); - $details = openssl_pkey_get_details($resource); - is_array($details) || throw AttestationStatementVerificationException::create( - 'Unable to read the certificate' - ); - //Check that authData publicKey matches the public key in the attestation certificate $attestedCredentialData = $authenticatorData->attestedCredentialData; $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( @@ -140,38 +143,50 @@ private function checkCertificateAndGetPublicKey( 'Unsupported key type' ); + /*---------------------------*/ + $cert = Certificate::fromPEM(PEM::fromString($certificate)); + //We check the attested key corresponds to the key in the certificate - $publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create( + PEM::fromString( + $publicKey->asPEM() + )->string() === $cert->tbsCertificate()->subjectPublicKeyInfo()->toPEM()->string() || throw AttestationStatementVerificationException::create( 'Invalid key' ); - /*---------------------------*/ - $certDetails = openssl_x509_parse($certificate); + $extensions = $cert->tbsCertificate() + ->extensions(); //Find Apple Extension with OID "1.2.840.113635.100.8.2" in certificate extensions - is_array( - $certDetails - ) || throw AttestationStatementVerificationException::create('The certificate is not valid'); - array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create( - 'The certificate has no extension' + $extensions->has(self::OID_APPLE) || throw AttestationStatementVerificationException::create( + 'The certificate extension "' . self::OID_APPLE . '" is missing' + ); + /** @var UnknownExtension $appleExtension */ + $appleExtension = $extensions->get(self::OID_APPLE); + $extensionSequence = Sequence::fromDER($appleExtension->extensionValue()); + $extensionSequence->has(0) || throw AttestationStatementVerificationException::create( + 'The certificate extension "' . self::OID_APPLE . '" is message' ); - is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create( - 'The certificate has no extension' + $firstExtension = $extensionSequence->at(0); + $firstExtension->isTagged() || throw AttestationStatementVerificationException::create( + 'The certificate extension "' . self::OID_APPLE . '" is invalid' ); - array_key_exists( - '1.2.840.113635.100.8.2', - $certDetails['extensions'] - ) || throw AttestationStatementVerificationException::create( - 'The certificate extension "1.2.840.113635.100.8.2" is missing' + $taggedExtension = $firstExtension->asTagged() + ->asElement(); + $taggedExtension instanceof ExplicitTagging || throw AttestationStatementVerificationException::create( + 'The certificate extension "' . self::OID_APPLE . '" is invalid' ); - $extension = $certDetails['extensions']['1.2.840.113635.100.8.2']; + $explicitExtension = $taggedExtension->explicit() + ->asElement(); + $explicitExtension instanceof OctetString || throw AttestationStatementVerificationException::create( + 'The certificate extension "' . self::OID_APPLE . '" is invalid' + ); + $extensionData = $explicitExtension->string(); $nonceToHash = $authenticatorData->authData . $clientDataHash; - $nonce = hash('sha256', $nonceToHash); + $nonce = hash('sha256', $nonceToHash, true); - //'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object - '3024a1220420' . $nonce === bin2hex( - (string) $extension - ) || throw AttestationStatementVerificationException::create('The client data hash is not valid'); + hash_equals($nonce, $extensionData) || throw AttestationStatementVerificationException::create( + 'The client data hash is not valid' + ); } } diff --git a/src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php b/src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php index ec967e0c..78c12ab4 100644 --- a/src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php +++ b/src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php @@ -11,6 +11,13 @@ use Cose\Algorithms; use Cose\Key\Key; use Psr\EventDispatcher\EventDispatcherInterface; +use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString; +use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType; +use SpomkyLabs\Pki\CryptoEncoding\PEM; +use SpomkyLabs\Pki\X501\ASN1\AttributeType; +use SpomkyLabs\Pki\X509\Certificate\Certificate; +use SpomkyLabs\Pki\X509\Certificate\Extension\UnknownExtension; +use SpomkyLabs\Pki\X509\Certificate\TBSCertificate; use Webauthn\AuthenticatorData; use Webauthn\Event\AttestationStatementLoaded; use Webauthn\Event\CanDispatchEvents; @@ -26,13 +33,14 @@ use Webauthn\Util\CoseSignatureFixer; use function array_key_exists; use function count; -use function in_array; use function is_array; use function is_string; use function openssl_verify; final class PackedAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents { + private const OID_FIDO_GEN_CE_AAGUID = '1.3.6.1.4.1.45724.1.1.4'; + private readonly Decoder $decoder; private EventDispatcherInterface $dispatcher; @@ -148,45 +156,48 @@ private function loadEmptyType(array $attestation): AttestationStatement return $attestationStatement; } + // https://www.w3.org/TR/webauthn-3/#sctn-packed-attestation-cert-requirements private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void { - $parsed = openssl_x509_parse($attestnCert); - is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate'); + $certificate = Certificate::fromPEM(PEM::fromString($attestnCert)); + $tbsCertificate = $certificate->tbsCertificate(); - //Check version - isset($parsed['version']) || throw AttestationStatementVerificationException::create( - 'Invalid certificate version' - ); - $parsed['version'] === 2 || throw AttestationStatementVerificationException::create( + //Check version (X.509 version 3 is encoded as 2) + $tbsCertificate->version() === TBSCertificate::VERSION_3 || throw AttestationStatementVerificationException::create( 'Invalid certificate version' ); //Check subject field - isset($parsed['name']) || throw AttestationStatementVerificationException::create( - 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"' - ); - str_contains( - (string) $parsed['name'], - '/OU=Authenticator Attestation' - ) || throw AttestationStatementVerificationException::create( + $subject = $tbsCertificate->subject(); + $subject->countOfType( + AttributeType::OID_COUNTRY_NAME + ) > 0 || throw AttestationStatementVerificationException::create('Certificate Subject-C must be set'); + $subject->countOfType( + AttributeType::OID_ORGANIZATION_NAME + ) > 0 || throw AttestationStatementVerificationException::create('Certificate Subject-O must be set'); + $subject->countOfType( + AttributeType::OID_ORGANIZATIONAL_UNIT_NAME + ) > 0 || throw AttestationStatementVerificationException::create('Certificate Subject-OU must be set'); + $subject->countOfType( + AttributeType::OID_COMMON_NAME + ) > 0 || throw AttestationStatementVerificationException::create('Certificate Subject-CN must be set'); + $ouValue = $subject->firstValueOf('OU') + ->stringValue(); + $ouValue === 'Authenticator Attestation' || throw AttestationStatementVerificationException::create( 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"' ); //Check extensions - isset($parsed['extensions']) || throw AttestationStatementVerificationException::create( - 'Certificate extensions are missing' - ); - is_array($parsed['extensions']) || throw AttestationStatementVerificationException::create( - 'Certificate extensions are missing' - ); + $extensions = $tbsCertificate->extensions(); //Check certificate is not a CA cert - isset($parsed['extensions']['basicConstraints']) || throw AttestationStatementVerificationException::create( - 'The Basic Constraints extension must have the CA component set to false' - ); - $parsed['extensions']['basicConstraints'] === 'CA:FALSE' || throw AttestationStatementVerificationException::create( + $extensions->hasBasicConstraints() || throw AttestationStatementVerificationException::create( 'The Basic Constraints extension must have the CA component set to false' ); + ! $extensions->basicConstraints() + ->isCA() || throw AttestationStatementVerificationException::create( + 'The Basic Constraints extension must have the CA component set to false' + ); $attestedCredentialData = $authenticatorData->attestedCredentialData; $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( @@ -194,13 +205,24 @@ private function checkCertificate(string $attestnCert, AuthenticatorData $authen ); // id-fido-gen-ce-aaguid OID check - if (in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true)) { + if ($extensions->has(self::OID_FIDO_GEN_CE_AAGUID)) { + /** @var UnknownExtension $aaguidExtension */ + $aaguidExtension = $extensions->get(self::OID_FIDO_GEN_CE_AAGUID); + ! $aaguidExtension->isCritical() || throw AttestationStatementVerificationException::create( + 'Extension ' . self::OID_FIDO_GEN_CE_AAGUID . ' must not be marked as critical' + ); + + $aaguidElement = UnspecifiedType::fromDER($aaguidExtension->extensionValue())->asElement(); + $aaguidElement instanceof OctetString || throw AttestationStatementVerificationException::create( + 'Invalid ' . self::OID_FIDO_GEN_CE_AAGUID . ' extension format' + ); + $aaguidValue = $aaguidElement->string(); hash_equals( $attestedCredentialData->aaguid ->toBinary(), - $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4'] + $aaguidValue ) || throw AttestationStatementVerificationException::create( - 'The value of the "aaguid" does not match with the certificate' + 'The value of the "aaguid" does not match with the certificate extension ' . self::OID_FIDO_GEN_CE_AAGUID ); } } diff --git a/src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php b/src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php index feeb9fc7..5a38f84a 100644 --- a/src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php +++ b/src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php @@ -15,6 +15,12 @@ use ParagonIE\ConstantTime\Base64UrlSafe; use Psr\Clock\ClockInterface; use Psr\EventDispatcher\EventDispatcherInterface; +use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString; +use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType; +use SpomkyLabs\Pki\CryptoEncoding\PEM; +use SpomkyLabs\Pki\X509\Certificate\Certificate; +use SpomkyLabs\Pki\X509\Certificate\Extension\UnknownExtension; +use SpomkyLabs\Pki\X509\Certificate\TBSCertificate; use Symfony\Component\Clock\NativeClock; use Webauthn\AuthenticatorData; use Webauthn\Event\AttestationStatementLoaded; @@ -28,15 +34,16 @@ use Webauthn\TrustPath\CertificateTrustPath; use function array_key_exists; use function count; -use function in_array; -use function is_array; -use function is_int; use function openssl_verify; use function sprintf; use function unpack; final class TPMAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents { + private const OID_FIDO_GEN_CE_AAGUID = '1.3.6.1.4.1.45724.1.1.4'; + + private const OID_AIK_CERTIFICATE = '2.23.133.8.3'; + private EventDispatcherInterface $dispatcher; private readonly ClockInterface $clock; @@ -348,79 +355,75 @@ private function processWithCertificate( return $result === 1; } + // https://www.w3.org/TR/webauthn-3/#sctn-tpm-cert-requirements private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void { - $parsed = openssl_x509_parse($attestnCert); - is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate'); + $certificate = Certificate::fromPEM(PEM::fromString($attestnCert)); + $tbsCertificate = $certificate->tbsCertificate(); - //Check version - (isset($parsed['version']) && $parsed['version'] === 2) || throw AttestationStatementVerificationException::create( + // Version MUST be set to 3 (X.509 version 3 is encoded as 2) + $tbsCertificate->version() === TBSCertificate::VERSION_3 || throw AttestationStatementVerificationException::create( 'Invalid certificate version' ); - //Check subject field is empty - isset($parsed['subject']) || throw AttestationStatementVerificationException::create( - 'Invalid certificate name. The Subject should be empty' - ); - is_array($parsed['subject']) || throw AttestationStatementVerificationException::create( - 'Invalid certificate name. The Subject should be empty' - ); - count($parsed['subject']) === 0 || throw AttestationStatementVerificationException::create( - 'Invalid certificate name. The Subject should be empty' - ); + // Subject field MUST be set to empty + $tbsCertificate->subject() + ->count() === 0 || throw AttestationStatementVerificationException::create( + 'Invalid certificate name. The Subject should be empty' + ); // Check period of validity - array_key_exists( - 'validFrom_time_t', - $parsed - ) || throw AttestationStatementVerificationException::create('Invalid certificate start date.'); - is_int($parsed['validFrom_time_t']) || throw AttestationStatementVerificationException::create( - 'Invalid certificate start date.' - ); - $startDate = (new DateTimeImmutable('now'))->setTimestamp($parsed['validFrom_time_t']); + $validity = $tbsCertificate->validity(); + $startDate = DateTimeImmutable::createFromInterface($validity->notBefore()->dateTime()); $startDate < $this->clock->now() || throw AttestationStatementVerificationException::create( 'Invalid certificate start date.' ); - array_key_exists('validTo_time_t', $parsed) || throw AttestationStatementVerificationException::create( - 'Invalid certificate end date.' - ); - is_int($parsed['validTo_time_t']) || throw AttestationStatementVerificationException::create( - 'Invalid certificate end date.' - ); - $endDate = (new DateTimeImmutable('now'))->setTimestamp($parsed['validTo_time_t']); + $endDate = DateTimeImmutable::createFromInterface($validity->notAfter()->dateTime()); $endDate > $this->clock->now() || throw AttestationStatementVerificationException::create( 'Invalid certificate end date.' ); - //Check extensions - (isset($parsed['extensions']) && is_array( - $parsed['extensions'] - )) || throw AttestationStatementVerificationException::create('Certificate extensions are missing'); + // Check extensions + $extensions = $tbsCertificate->extensions(); - //Check subjectAltName - isset($parsed['extensions']['subjectAltName']) || throw AttestationStatementVerificationException::create( - 'The "subjectAltName" is missing' + // Check Subject Alternative Name extension + $extensions->hasSubjectAlternativeName() || throw AttestationStatementVerificationException::create( + 'The Subject Alternative Name extension must be set' ); - //Check extendedKeyUsage - isset($parsed['extensions']['extendedKeyUsage']) || throw AttestationStatementVerificationException::create( - 'The "subjectAltName" is missing' + // Check Extended Key Usage extension MUST contain the OID 2.23.133.8.3 + $extensions->hasExtendedKeyUsage() || throw AttestationStatementVerificationException::create( + 'The Extended Key Usage extensions must contain ' . self::OID_AIK_CERTIFICATE, ); - in_array( - $parsed['extensions']['extendedKeyUsage'], - ['2.23.133.8.3', 'Attestation Identity Key Certificate'], - true - ) || throw AttestationStatementVerificationException::create('The "extendedKeyUsage" is invalid'); + $extendedKeyUsage = $extensions->extendedKeyUsage(); + $extendedKeyUsage->has(self::OID_AIK_CERTIFICATE) || throw AttestationStatementVerificationException::create( + 'The Extended Key Usage extensions must contain ' . self::OID_AIK_CERTIFICATE, + ); + + // The Basic Constraints extension MUST have the CA component set to false. + $extensions->basicConstraints() + ->isCA() === false || throw AttestationStatementVerificationException::create( + 'The Basic Constraints extension must have the CA component set to false' + ); // id-fido-gen-ce-aaguid OID check - in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && ! hash_equals( - $authenticatorData->attestedCredentialData - ?->aaguid - ->toBinary() ?? '', - $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4'] - ) && throw AttestationStatementVerificationException::create( - 'The value of the "aaguid" does not match with the certificate' - ); + if ($extensions->has(self::OID_FIDO_GEN_CE_AAGUID)) { + /** @var UnknownExtension $aaguidExtension */ + $aaguidExtension = $extensions->get(self::OID_FIDO_GEN_CE_AAGUID); + $aaguidElement = UnspecifiedType::fromDER($aaguidExtension->extensionValue())->asElement(); + $aaguidElement instanceof OctetString || throw AttestationStatementVerificationException::create( + 'Invalid ' . self::OID_FIDO_GEN_CE_AAGUID . ' extension format' + ); + $aaguidValue = $aaguidElement->string(); + hash_equals( + $authenticatorData->attestedCredentialData + ?->aaguid + ->toBinary() ?? '', + $aaguidValue + ) || throw AttestationStatementVerificationException::create( + 'The value of the "aaguid" does not match with the certificate' + ); + } } }