Skip to content

Commit 77b6650

Browse files
Spomkyzll600
andauthored
fix: use spomky-labs/pki-framework to replace native php openssl functions for attestation statements (backport to 5.2.x) (#790)
* refactor: use spomky-labs/pki-framework to replace native php openssl functions for attestation statements * chore: apply code style fixes and update PHPStan baseline - Fix code style (ECS) in AndroidKey and Apple attestation statement supports - Regenerate PHPStan baseline to account for removed openssl_* function calls --------- Co-authored-by: zll600 <3400692417@qq.com>
1 parent aabe72a commit 77b6650

File tree

5 files changed

+202
-195
lines changed

5 files changed

+202
-195
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,12 +1125,6 @@ parameters:
11251125
count: 1
11261126
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php
11271127

1128-
-
1129-
rawMessage: 'Parameter #1 $data of static method SpomkyLabs\Pki\ASN1\Element::fromDER() expects string, mixed given.'
1130-
identifier: argument.type
1131-
count: 1
1132-
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php
1133-
11341128
-
11351129
rawMessage: 'Parameter #1 $data of static method Webauthn\MetadataService\CertificateChain\CertificateToolbox::convertAllDERToPEM() expects array<string>, mixed given.'
11361130
identifier: argument.type
@@ -1143,12 +1137,6 @@ parameters:
11431137
count: 1
11441138
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php
11451139

1146-
-
1147-
rawMessage: 'Parameter #1 $key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey|false given.'
1148-
identifier: argument.type
1149-
count: 1
1150-
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php
1151-
11521140
-
11531141
rawMessage: 'Parameter #2 $array of function array_key_exists expects array, mixed given.'
11541142
identifier: argument.type
@@ -1167,12 +1155,6 @@ parameters:
11671155
count: 1
11681156
path: src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php
11691157

1170-
-
1171-
rawMessage: Cannot cast mixed to string.
1172-
identifier: cast.string
1173-
count: 1
1174-
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php
1175-
11761158
-
11771159
rawMessage: 'Parameter #1 $data of static method Cose\Key\Key::createFromData() expects array<int, mixed>, mixed given.'
11781160
identifier: argument.type
@@ -1191,12 +1173,6 @@ parameters:
11911173
count: 1
11921174
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php
11931175

1194-
-
1195-
rawMessage: 'Parameter #1 $key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey|false given.'
1196-
identifier: argument.type
1197-
count: 1
1198-
path: src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php
1199-
12001176
-
12011177
rawMessage: 'Parameter #2 $array of function array_key_exists expects array, mixed given.'
12021178
identifier: argument.type
@@ -1293,12 +1269,6 @@ parameters:
12931269
count: 3
12941270
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php
12951271

1296-
-
1297-
rawMessage: Cannot cast mixed to string.
1298-
identifier: cast.string
1299-
count: 1
1300-
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php
1301-
13021272
-
13031273
rawMessage: 'Parameter #1 $data of static method Webauthn\MetadataService\CertificateChain\CertificateToolbox::convertAllDERToPEM() expects array<string>, array<mixed, mixed> given.'
13041274
identifier: argument.type
@@ -1347,12 +1317,6 @@ parameters:
13471317
count: 1
13481318
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php
13491319

1350-
-
1351-
rawMessage: 'Parameter #2 $user_string of function hash_equals expects string, mixed given.'
1352-
identifier: argument.type
1353-
count: 1
1354-
path: src/webauthn/src/AttestationStatement/PackedAttestationStatementSupport.php
1355-
13561320
-
13571321
rawMessage: Cannot access offset 'certInfo' on mixed.
13581322
identifier: offsetAccess.nonOffsetAccessible
@@ -1473,12 +1437,6 @@ parameters:
14731437
count: 1
14741438
path: src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php
14751439

1476-
-
1477-
rawMessage: 'Parameter #2 $user_string of function hash_equals expects string, mixed given.'
1478-
identifier: argument.type
1479-
count: 1
1480-
path: src/webauthn/src/AttestationStatement/TPMAttestationStatementSupport.php
1481-
14821440
-
14831441
rawMessage: 'Parameter #1 $callback of function array_map expects (callable(mixed, int|string): mixed)|null, Closure(mixed, string): Webauthn\AuthenticationExtensions\AuthenticationExtension given.'
14841442
identifier: argument.type

src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

Lines changed: 50 additions & 42 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,83 @@ 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(
156-
'Invalid key'
157-
);
158160

159161
/*---------------------------*/
160-
$certDetails = openssl_x509_parse($certificate);
161-
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'
168-
);
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);
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($publicKey->asPEM())->string() === $cert->tbsCertificate()
169+
->subjectPublicKeyInfo()
170+
->toPEM()
171+
->string() || throw AttestationStatementVerificationException::create('Invalid key');
172+
173+
$extensions = $cert->tbsCertificate()
174+
->extensions();
175+
176+
//Find Android KeyStore Extension with OID self::OID_ANDROID in certificate extensions
177+
$extensions->has(self::OID_ANDROID) || throw AttestationStatementVerificationException::create(
178+
'The certificate extension "' . self::OID_ANDROID . '" is missing'
179+
);
180+
/** @var UnknownExtension $androidExtension */
181+
$androidExtension = $extensions->get(self::OID_ANDROID);
182+
/**
183+
* Parse the Android extension value structure
184+
* @see https://source.android.com/docs/security/features/keystore/attestation#attestation-extension
185+
*/
186+
$extensionAsAsn1 = Sequence::fromDER($androidExtension->extensionValue());
181187

182188
//Check that attestationChallenge is set to the clientDataHash.
183189
$extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create(
184-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
190+
'The attestationChallenge field is missing'
185191
);
186192
$ext = $extensionAsAsn1->at(4)
187193
->asElement();
188194
$ext instanceof OctetString || throw AttestationStatementVerificationException::create(
189-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
195+
'The attestationChallenge field must be an OctetString'
190196
);
191197
$clientDataHash === $ext->string() || throw AttestationStatementVerificationException::create(
192198
'The client data hash is not valid'
193199
);
194200

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

200206
$softwareEnforcedFlags = $extensionAsAsn1->at(6)
201207
->asElement();
202208
$softwareEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
203-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
209+
'The softwareEnforced field must be a Sequence'
204210
);
205211
$this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags);
206212

207213
$extensionAsAsn1->has(7) || throw AttestationStatementVerificationException::create(
208-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
214+
'The teeEnforced field is missing'
209215
);
210216
$teeEnforcedFlags = $extensionAsAsn1->at(7)
211217
->asElement();
212218
$teeEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create(
213-
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid'
219+
'The teeEnforced field must be a Sequence'
214220
);
215221
$this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags);
216222
}
217223

218224
private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void
219225
{
220226
foreach ($sequence->elements() as $tag) {
221-
$tag->asElement() instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
227+
$element = $tag->asElement();
228+
$element instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
222229
'Invalid tag'
223230
);
224-
$tag->asElement()
225-
->tag() !== 600 || throw AttestationStatementVerificationException::create('Forbidden tag 600 found');
231+
$element->tag() !== self::ANDROID_TAG_ALL_APPLICATIONS || throw AttestationStatementVerificationException::create(
232+
'The allApplications tag (' . self::ANDROID_TAG_ALL_APPLICATIONS . ') is forbidden - key must be bound to specific application'
233+
);
226234
}
227235
}
228236
}

src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php

Lines changed: 45 additions & 31 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,49 @@ 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(
145-
'Invalid key'
146-
);
150+
PEM::fromString($publicKey->asPEM())->string() === $cert->tbsCertificate()
151+
->subjectPublicKeyInfo()
152+
->toPEM()
153+
->string() || throw AttestationStatementVerificationException::create('Invalid key');
147154

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

151158
//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'
159+
$extensions->has(self::OID_APPLE) || throw AttestationStatementVerificationException::create(
160+
'The certificate extension "' . self::OID_APPLE . '" is missing'
157161
);
158-
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
159-
'The certificate has no extension'
162+
/** @var UnknownExtension $appleExtension */
163+
$appleExtension = $extensions->get(self::OID_APPLE);
164+
$extensionSequence = Sequence::fromDER($appleExtension->extensionValue());
165+
$extensionSequence->has(0) || throw AttestationStatementVerificationException::create(
166+
'The certificate extension "' . self::OID_APPLE . '" is message'
160167
);
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'
168+
$firstExtension = $extensionSequence->at(0);
169+
$firstExtension->isTagged() || throw AttestationStatementVerificationException::create(
170+
'The certificate extension "' . self::OID_APPLE . '" is invalid'
166171
);
167-
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
172+
$taggedExtension = $firstExtension->asTagged()
173+
->asElement();
174+
$taggedExtension instanceof ExplicitTagging || throw AttestationStatementVerificationException::create(
175+
'The certificate extension "' . self::OID_APPLE . '" is invalid'
176+
);
177+
$explicitExtension = $taggedExtension->explicit()
178+
->asElement();
179+
$explicitExtension instanceof OctetString || throw AttestationStatementVerificationException::create(
180+
'The certificate extension "' . self::OID_APPLE . '" is invalid'
181+
);
182+
$extensionData = $explicitExtension->string();
168183

169184
$nonceToHash = $authenticatorData->authData . $clientDataHash;
170-
$nonce = hash('sha256', $nonceToHash);
185+
$nonce = hash('sha256', $nonceToHash, true);
171186

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');
187+
hash_equals($nonce, $extensionData) || throw AttestationStatementVerificationException::create(
188+
'The client data hash is not valid'
189+
);
176190
}
177191
}

0 commit comments

Comments
 (0)