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,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}
0 commit comments