Skip to content

Commit 46bcb7b

Browse files
committed
refactor: use spomky-labs/pki-framework to replace native php openssl functions for attestation statements
1 parent 4d7c777 commit 46bcb7b

File tree

4 files changed

+200
-151
lines changed

4 files changed

+200
-151
lines changed

src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
1515
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
1616
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging;
17+
use SpomkyLabs\Pki\CryptoEncoding\PEM;
18+
use SpomkyLabs\Pki\X509\Certificate\Certificate;
19+
use SpomkyLabs\Pki\X509\Certificate\Extension\UnknownExtension;
1720
use Webauthn\AuthenticatorData;
1821
use Webauthn\Event\AttestationStatementLoaded;
1922
use Webauthn\Event\CanDispatchEvents;
@@ -26,13 +29,19 @@
2629
use Webauthn\TrustPath\CertificateTrustPath;
2730
use function array_key_exists;
2831
use function count;
29-
use function is_array;
30-
use function openssl_pkey_get_public;
3132
use function openssl_verify;
3233
use function sprintf;
3334

3435
final class AndroidKeyAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
3536
{
37+
private const OID_ANDROID = '1.3.6.1.4.1.11129.2.1.17';
38+
39+
/**
40+
* Tag 600 (allApplications)
41+
* @see https://source.android.com/docs/security/features/keystore/attestation#version-1
42+
*/
43+
private const ANDROID_TAG_ALL_APPLICATIONS = 600;
44+
3645
private readonly Decoder $decoder;
3746

3847
private EventDispatcherInterface $dispatcher;
@@ -117,17 +126,14 @@ public function isValid(
117126
) === 1;
118127
}
119128

129+
/**
130+
* @see https://www.w3.org/TR/webauthn-3/#sctn-android-key-attestation
131+
*/
120132
private function checkCertificate(
121133
string $certificate,
122134
string $clientDataHash,
123135
AuthenticatorData $authenticatorData
124136
): void {
125-
$resource = openssl_pkey_get_public($certificate);
126-
$details = openssl_pkey_get_details($resource);
127-
is_array($details) || throw AttestationStatementVerificationException::create(
128-
'Unable to read the certificate'
129-
);
130-
131137
//Check that authData publicKey matches the public key in the attestation certificate
132138
$attestedCredentialData = $authenticatorData->attestedCredentialData;
133139
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
@@ -148,81 +154,84 @@ private function checkCertificate(
148154
);
149155
$publicDataStream->close();
150156
$publicKey = Key::createFromData($coseKey->normalize());
151-
152157
($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create(
153158
'Unsupported key type'
154159
);
155-
$publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create(
160+
161+
/*---------------------------*/
162+
/**
163+
* @see https://w3c.github.io/webauthn/#sctn-key-attstn-cert-requirements
164+
* @see https://source.android.com/docs/security/features/keystore/attestation#attestation-certificate
165+
*/
166+
$cert = Certificate::fromPEM(PEM::fromString($certificate));
167+
//We check the attested key corresponds to the key in the certificate
168+
PEM::fromString(
169+
$publicKey->asPEM()
170+
)->string() === $cert->tbsCertificate()->subjectPublicKeyInfo()->toPEM()->string() || throw AttestationStatementVerificationException::create(
156171
'Invalid key'
157172
);
158173

159-
/*---------------------------*/
160-
$certDetails = openssl_x509_parse($certificate);
174+
$extensions = $cert->tbsCertificate()
175+
->extensions();
161176

162-
//Find Android KeyStore Extension with OID "1.3.6.1.4.1.11129.2.1.17" in certificate extensions
163-
is_array(
164-
$certDetails
165-
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
166-
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
167-
'The certificate has no extension'
177+
//Find Android KeyStore Extension with OID self::OID_ANDROID in certificate extensions
178+
$extensions->has(self::OID_ANDROID) || throw AttestationStatementVerificationException::create(
179+
'The certificate extension "' . self::OID_ANDROID . '" is missing'
168180
);
169-
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
170-
'The certificate has no extension'
171-
);
172-
array_key_exists(
173-
'1.3.6.1.4.1.11129.2.1.17',
174-
$certDetails['extensions']
175-
) || throw AttestationStatementVerificationException::create(
176-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing'
177-
);
178-
$extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17'];
179-
$extensionAsAsn1 = Sequence::fromDER($extension);
180-
$extensionAsAsn1->has(4);
181+
/** @var UnknownExtension $androidExtension */
182+
$androidExtension = $extensions->get(self::OID_ANDROID);
183+
/**
184+
* Parse the Android extension value structure
185+
* @see https://source.android.com/docs/security/features/keystore/attestation#attestation-extension
186+
*/
187+
$extensionAsAsn1 = Sequence::fromDER($androidExtension->extensionValue());
181188

182189
//Check that attestationChallenge is set to the clientDataHash.
183190
$extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create(
184-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
191+
'The attestationChallenge field is missing'
185192
);
186193
$ext = $extensionAsAsn1->at(4)
187194
->asElement();
188195
$ext instanceof OctetString || throw AttestationStatementVerificationException::create(
189-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
196+
'The attestationChallenge field must be an OctetString'
190197
);
191198
$clientDataHash === $ext->string() || throw AttestationStatementVerificationException::create(
192199
'The client data hash is not valid'
193200
);
194201

195202
//Check that both teeEnforced and softwareEnforced structures don't contain allApplications(600) tag.
196203
$extensionAsAsn1->has(6) || throw AttestationStatementVerificationException::create(
197-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
204+
'The softwareEnforced field is missing'
198205
);
199206

200207
$softwareEnforcedFlags = $extensionAsAsn1->at(6)
201208
->asElement();
202209
$softwareEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
203-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
210+
'The softwareEnforced field must be a Sequence'
204211
);
205212
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
206213

207214
$extensionAsAsn1->has(7) || throw AttestationStatementVerificationException::create(
208-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
215+
'The teeEnforced field is missing'
209216
);
210217
$teeEnforcedFlags = $extensionAsAsn1->at(7)
211218
->asElement();
212219
$teeEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
213-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
220+
'The teeEnforced field must be a Sequence'
214221
);
215222
$this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags);
216223
}
217224

218225
private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void
219226
{
220227
foreach ($sequence->elements() as $tag) {
221-
$tag->asElement() instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
228+
$element = $tag->asElement();
229+
$element instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
222230
'Invalid tag'
223231
);
224-
$tag->asElement()
225-
->tag() !== 600 || throw AttestationStatementVerificationException::create('Forbidden tag 600 found');
232+
$element->tag() !== self::ANDROID_TAG_ALL_APPLICATIONS || throw AttestationStatementVerificationException::create(
233+
'The allApplications tag (' . self::ANDROID_TAG_ALL_APPLICATIONS . ') is forbidden - key must be bound to specific application'
234+
);
226235
}
227236
}
228237
}

src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
use Cose\Key\Key;
1111
use Cose\Key\RsaKey;
1212
use Psr\EventDispatcher\EventDispatcherInterface;
13+
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
14+
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
15+
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging;
16+
use SpomkyLabs\Pki\CryptoEncoding\PEM;
17+
use SpomkyLabs\Pki\X509\Certificate\Certificate;
18+
use SpomkyLabs\Pki\X509\Certificate\Extension\UnknownExtension;
1319
use Webauthn\AuthenticatorData;
1420
use Webauthn\Event\AttestationStatementLoaded;
1521
use Webauthn\Event\CanDispatchEvents;
@@ -22,11 +28,11 @@
2228
use Webauthn\TrustPath\CertificateTrustPath;
2329
use function array_key_exists;
2430
use function count;
25-
use function is_array;
26-
use function openssl_pkey_get_public;
2731

2832
final class AppleAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
2933
{
34+
private const OID_APPLE = '1.2.840.113635.100.8.2';
35+
3036
private readonly Decoder $decoder;
3137

3238
private EventDispatcherInterface $dispatcher;
@@ -105,17 +111,14 @@ public function isValid(
105111
return true;
106112
}
107113

114+
/**
115+
* @see https://www.w3.org/TR/webauthn-3/#sctn-apple-anonymous-attestation
116+
*/
108117
private function checkCertificateAndGetPublicKey(
109118
string $certificate,
110119
string $clientDataHash,
111120
AuthenticatorData $authenticatorData
112121
): void {
113-
$resource = openssl_pkey_get_public($certificate);
114-
$details = openssl_pkey_get_details($resource);
115-
is_array($details) || throw AttestationStatementVerificationException::create(
116-
'Unable to read the certificate'
117-
);
118-
119122
//Check that authData publicKey matches the public key in the attestation certificate
120123
$attestedCredentialData = $authenticatorData->attestedCredentialData;
121124
$attestedCredentialData !== null || throw AttestationStatementVerificationException::create(
@@ -140,38 +143,50 @@ private function checkCertificateAndGetPublicKey(
140143
'Unsupported key type'
141144
);
142145

146+
/*---------------------------*/
147+
$cert = Certificate::fromPEM(PEM::fromString($certificate));
148+
143149
//We check the attested key corresponds to the key in the certificate
144-
$publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create(
150+
PEM::fromString(
151+
$publicKey->asPEM()
152+
)->string() === $cert->tbsCertificate()->subjectPublicKeyInfo()->toPEM()->string() || throw AttestationStatementVerificationException::create(
145153
'Invalid key'
146154
);
147155

148-
/*---------------------------*/
149-
$certDetails = openssl_x509_parse($certificate);
156+
$extensions = $cert->tbsCertificate()
157+
->extensions();
150158

151159
//Find Apple Extension with OID "1.2.840.113635.100.8.2" in certificate extensions
152-
is_array(
153-
$certDetails
154-
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
155-
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
156-
'The certificate has no extension'
160+
$extensions->has(self::OID_APPLE) || throw AttestationStatementVerificationException::create(
161+
'The certificate extension "' . self::OID_APPLE . '" is missing'
162+
);
163+
/** @var UnknownExtension $appleExtension */
164+
$appleExtension = $extensions->get(self::OID_APPLE);
165+
$extensionSequence = Sequence::fromDER($appleExtension->extensionValue());
166+
$extensionSequence->has(0) || throw AttestationStatementVerificationException::create(
167+
'The certificate extension "' . self::OID_APPLE . '" is message'
157168
);
158-
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
159-
'The certificate has no extension'
169+
$firstExtension = $extensionSequence->at(0);
170+
$firstExtension->isTagged() || throw AttestationStatementVerificationException::create(
171+
'The certificate extension "' . self::OID_APPLE . '" is invalid'
160172
);
161-
array_key_exists(
162-
'1.2.840.113635.100.8.2',
163-
$certDetails['extensions']
164-
) || throw AttestationStatementVerificationException::create(
165-
'The certificate extension "1.2.840.113635.100.8.2" is missing'
173+
$taggedExtension = $firstExtension->asTagged()
174+
->asElement();
175+
$taggedExtension instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
176+
'The certificate extension "' . self::OID_APPLE . '" is invalid'
166177
);
167-
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
178+
$explicitExtension = $taggedExtension->explicit()
179+
->asElement();
180+
$explicitExtension instanceof OctetString || throw AttestationStatementVerificationException::create(
181+
'The certificate extension "' . self::OID_APPLE . '" is invalid'
182+
);
183+
$extensionData = $explicitExtension->string();
168184

169185
$nonceToHash = $authenticatorData->authData . $clientDataHash;
170-
$nonce = hash('sha256', $nonceToHash);
186+
$nonce = hash('sha256', $nonceToHash, true);
171187

172-
//'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object
173-
'3024a1220420' . $nonce === bin2hex(
174-
(string) $extension
175-
) || throw AttestationStatementVerificationException::create('The client data hash is not valid');
188+
hash_equals($nonce, $extensionData) || throw AttestationStatementVerificationException::create(
189+
'The client data hash is not valid'
190+
);
176191
}
177192
}

0 commit comments

Comments
 (0)