Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1125,12 +1125,6 @@ parameters:
count: 1
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $data of static method SpomkyLabs\Pki\ASN1\Element::fromDER() expects string, mixed given.'
identifier: argument.type
count: 1
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $data of static method Webauthn\MetadataService\CertificateChain\CertificateToolbox::convertAllDERToPEM() expects array<string>, mixed given.'
identifier: argument.type
Expand All @@ -1143,12 +1137,6 @@ parameters:
count: 1
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey|false given.'
identifier: argument.type
count: 1
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

-
rawMessage: 'Parameter #2 $array of function array_key_exists expects array, mixed given.'
identifier: argument.type
Expand All @@ -1167,12 +1155,6 @@ parameters:
count: 1
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

-
rawMessage: Cannot cast mixed to string.
identifier: cast.string
count: 1
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $data of static method Cose\Key\Key::createFromData() expects array<int, mixed>, mixed given.'
identifier: argument.type
Expand All @@ -1191,12 +1173,6 @@ parameters:
count: 1
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey|false given.'
identifier: argument.type
count: 1
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php

-
rawMessage: 'Parameter #2 $array of function array_key_exists expects array, mixed given.'
identifier: argument.type
Expand Down Expand Up @@ -1293,12 +1269,6 @@ parameters:
count: 3
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php

-
rawMessage: Cannot cast mixed to string.
identifier: cast.string
count: 1
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $data of static method Webauthn\MetadataService\CertificateChain\CertificateToolbox::convertAllDERToPEM() expects array<string>, array<mixed, mixed> given.'
identifier: argument.type
Expand Down Expand Up @@ -1347,12 +1317,6 @@ parameters:
count: 1
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php

-
rawMessage: 'Parameter #2 $user_string of function hash_equals expects string, mixed given.'
identifier: argument.type
count: 1
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php

-
rawMessage: Cannot access offset 'certInfo' on mixed.
identifier: offsetAccess.nonOffsetAccessible
Expand Down Expand Up @@ -1473,12 +1437,6 @@ parameters:
count: 1
path: src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php

-
rawMessage: 'Parameter #2 $user_string of function hash_equals expects string, mixed given.'
identifier: argument.type
count: 1
path: src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php

-
rawMessage: 'Parameter #1 $callback of function array_map expects (callable(mixed, int|string): mixed)|null, Closure(mixed, string): Webauthn\AuthenticationExtensions\AuthenticationExtension given.'
identifier: argument.type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -148,81 +154,83 @@ 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(
'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
is_array(
$certDetails
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
'The certificate has no extension'
);
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);
/**
* @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');

$extensions = $cert->tbsCertificate()
->extensions();

//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'
);
/** @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'
);

//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);
}

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'
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -140,38 +143,49 @@ 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(
'Invalid key'
);
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'
);
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
'The certificate has no extension'
/** @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'
);
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'
$firstExtension = $extensionSequence->at(0);
$firstExtension->isTagged() || throw AttestationStatementVerificationException::create(
'The certificate extension "' . self::OID_APPLE . '" is invalid'
);
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
$taggedExtension = $firstExtension->asTagged()
->asElement();
$taggedExtension instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
'The certificate extension "' . self::OID_APPLE . '" is invalid'
);
$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'
);
}
}
Loading
Loading