1414use SpomkyLabs \Pki \ASN1 \Type \Constructed \Sequence ;
1515use SpomkyLabs \Pki \ASN1 \Type \Primitive \OctetString ;
1616use 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 ;
1720use Webauthn \AuthenticatorData ;
1821use Webauthn \Event \AttestationStatementLoaded ;
1922use Webauthn \Event \CanDispatchEvents ;
2629use Webauthn \TrustPath \CertificateTrustPath ;
2730use function array_key_exists ;
2831use function count ;
29- use function is_array ;
30- use function openssl_pkey_get_public ;
3132use function openssl_verify ;
3233use function sprintf ;
3334
3435final 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}
0 commit comments