@@ -89,68 +89,155 @@ public function process(
8989 $ attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create (
9090 'No attested credential data found '
9191 );
92+
93+ if (! $ this ->isAttestationVerificationRequested ($ publicKeyCredentialOptions )) {
94+ $ this ->logger ->debug ('No attestation verification requested by RP. ' );
95+ return ;
96+ }
97+
98+ if ($ attestationStatement ->type === AttestationStatement::TYPE_NONE ) {
99+ $ this ->logger ->debug ('None attestation format. No metadata verification required. ' );
100+ return ;
101+ }
102+
92103 $ aaguid = $ attestedCredentialData ->aaguid
93104 ->__toString ();
94- if ($ publicKeyCredentialOptions ->attestation === null || $ publicKeyCredentialOptions ->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE ) {
95- $ this ->logger ->debug ('No attestation is asked. ' );
96- if ($ aaguid === '00000000-0000-0000-0000-000000000000 ' && in_array (
97- $ attestationStatement ->type ,
98- [AttestationStatement::TYPE_NONE , AttestationStatement::TYPE_SELF ],
99- true
100- )) {
101- $ this ->logger ->debug ('The Attestation Statement is anonymous. ' );
102- $ this ->checkCertificateChain ($ attestationStatement , null );
103- return ;
104- }
105+ if ($ this ->isNullAaguid ($ aaguid )) {
106+ $ this ->logger ->debug ('Null AAGUID detected. Skipping metadata verification. ' , [
107+ 'reason ' => 'Privacy placeholder or U2F device ' ,
108+ ]);
105109 return ;
106110 }
107- // If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000)
108- // => nothing to check
109- if ($ attestationStatement ->type === AttestationStatement::TYPE_NONE ) {
110- $ this ->logger ->debug ('No attestation returned. ' );
111- //No attestation is returned. We shall ensure that the AAGUID is a null one.
112- //if ($aaguid !== '00000000-0000-0000-0000-000000000000') {
113- //$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [
114- // 'aaguid' => $aaguid,
115- // 'AttestationStatement' => $attestationStatement,
116- //]);
117- //$attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000');
118- // return;
119- //}
111+
112+ if ($ attestationStatement ->type === AttestationStatement::TYPE_SELF ) {
113+ $ this ->logger ->debug ('Self attestation detected. ' , [
114+ 'aaguid ' => $ aaguid ,
115+ ]);
116+ $ this ->processSelfAttestation ($ aaguid );
120117 return ;
121118 }
122- if ($ aaguid === '00000000-0000-0000-0000-000000000000 ' ) {
123- //No need to continue if the AAGUID is null.
124- // This could be the case e.g. with AnonCA type
119+
120+ $ this ->logger ->debug ('Processing attestation with full metadata validation. ' , [
121+ 'type ' => $ attestationStatement ->type ,
122+ 'aaguid ' => $ aaguid ,
123+ ]);
124+ $ this ->processWithMetadata ($ attestationStatement , $ aaguid );
125+ }
126+
127+ /**
128+ * Check if RP requested attestation verification.
129+ * @see https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-none
130+ */
131+ private function isAttestationVerificationRequested (
132+ PublicKeyCredentialCreationOptions $ publicKeyCredentialOptions
133+ ): bool {
134+ return ! in_array (
135+ $ publicKeyCredentialOptions ->attestation ,
136+ [null , PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE ],
137+ true
138+ );
139+ }
140+
141+ /**
142+ * Check if AAGUID is all-zeros (privacy placeholder or U2F device).
143+ *
144+ * All-zeros AAGUID: either privacy placeholder or U2F device (which predates AAGUID).
145+ * 1) Privacy Placeholder indicates the authenticator does not provide detailed information.
146+ * 2) U2F device cannot provide useful AAGUID.
147+ *
148+ * So Metadata Statement lookup by AAGUID not possible.
149+ *
150+ * @see https://www.w3.org/TR/webauthn-3/#sctn-createCredential
151+ * @see https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html#u2f-authenticatorMakeCredential-interoperability
152+ */
153+ private function isNullAaguid (string $ aaguid ): bool
154+ {
155+ return $ aaguid === '00000000-0000-0000-0000-000000000000 ' ;
156+ }
157+
158+ /**
159+ * Process self attestation: check MDS for compromised devices.
160+ *
161+ * Self attestation: authenticator uses credential key pair to sign attestation.
162+ * No attestation certificate chain to validate, but we still check MDS for compromised devices.
163+ *
164+ * @see https://www.w3.org/TR/webauthn-3/#self
165+ */
166+ private function processSelfAttestation (string $ aaguid ): void
167+ {
168+ $ metadataStatement = $ this ->metadataStatementRepository ?->findOneByAAGUID($ aaguid );
169+ if ($ metadataStatement === null ) {
170+ $ this ->logger ->info ('No metadata statement found for self attestation. Skipping MDS verification. ' , [
171+ 'aaguid ' => $ aaguid ,
172+ ]);
125173 return ;
126174 }
127- //The MDS Repository is mandatory here
175+
176+ $ this ->logger ->debug ('Metadata statement found for self attestation. Checking status reports. ' , [
177+ 'aaguid ' => $ aaguid ,
178+ ]);
179+ $ this ->checkStatusReport ($ aaguid );
180+ // Note: We do NOT check attestationTypes for self attestation as the authenticator
181+ // is using its credential key (not attestation certificate), which may differ from
182+ // the declared attestation types in metadata (which describe certificate-based attestation).
183+ }
184+
185+ /**
186+ * Process attestation with full metadata validation (Basic, AttCA, AnonCA).
187+ */
188+ private function processWithMetadata (AttestationStatement $ attestationStatement , string $ aaguid ): void
189+ {
128190 $ this ->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create (
129191 'The Metadata Statement Repository is mandatory when requesting attestation objects. '
130192 );
131193 $ metadataStatement = $ this ->metadataStatementRepository ->findOneByAAGUID ($ aaguid );
132- // At this point, the Metadata Statement is mandatory
133194 $ metadataStatement !== null || throw AuthenticatorResponseVerificationException::create (
134195 sprintf ('The Metadata Statement for the AAGUID "%s" is missing ' , $ aaguid )
135196 );
136- // We check the last status report
197+
198+ $ this ->logger ->debug ('Metadata statement found. Starting validation. ' , [
199+ 'aaguid ' => $ aaguid ,
200+ 'attestation_type ' => $ attestationStatement ->type ,
201+ ]);
202+
137203 $ this ->checkStatusReport ($ aaguid );
138- // We check the certificate chain (if any)
204+ $ this ->logger ->debug ('Status report verification completed. ' );
205+
139206 $ this ->checkCertificateChain ($ attestationStatement , $ metadataStatement );
140- // Check Attestation Type is allowed
141- if (count ($ metadataStatement ->attestationTypes ) !== 0 ) {
142- $ type = $ this ->getAttestationType ($ attestationStatement );
143- in_array (
144- $ type ,
145- $ metadataStatement ->attestationTypes ,
146- true
147- ) || throw AuthenticatorResponseVerificationException::create (
148- sprintf (
149- 'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator. ' ,
150- $ type
151- )
152- );
207+ $ this ->logger ->debug ('Certificate chain verification completed. ' );
208+
209+ $ this ->checkAttestationTypeIsAllowed ($ attestationStatement , $ metadataStatement );
210+ $ this ->logger ->debug ('Attestation type verification completed. ' );
211+ }
212+
213+ /**
214+ * Check if the attestation type is allowed for this authenticator.
215+ */
216+ private function checkAttestationTypeIsAllowed (
217+ AttestationStatement $ attestationStatement ,
218+ MetadataStatement $ metadataStatement
219+ ): void {
220+ if (count ($ metadataStatement ->attestationTypes ) === 0 ) {
221+ $ this ->logger ->debug ('No attestation types restrictions in metadata statement. ' );
222+ return ;
153223 }
224+
225+ $ type = $ this ->getAttestationType ($ attestationStatement );
226+ $ this ->logger ->debug ('Checking attestation type. ' , [
227+ 'type ' => $ type ,
228+ 'allowed_types ' => $ metadataStatement ->attestationTypes ,
229+ ]);
230+
231+ in_array (
232+ $ type ,
233+ $ metadataStatement ->attestationTypes ,
234+ true
235+ ) || throw AuthenticatorResponseVerificationException::create (
236+ sprintf (
237+ 'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator. ' ,
238+ $ type
239+ )
240+ );
154241 }
155242
156243 private function getAttestationType (AttestationStatement $ attestationStatement ): string
@@ -169,29 +256,39 @@ private function checkStatusReport(string $aaguid): void
169256 $ statusReports = $ this ->statusReportRepository === null ? [] : $ this ->statusReportRepository ->findStatusReportsByAAGUID (
170257 $ aaguid
171258 );
172- if (count ($ statusReports ) !== 0 ) {
173- $ lastStatusReport = end ($ statusReports );
174- if ($ lastStatusReport ->isCompromised ()) {
175- throw AuthenticatorResponseVerificationException::create (
176- 'The authenticator is compromised and cannot be used '
177- );
178- }
259+ if (count ($ statusReports ) === 0 ) {
260+ $ this ->logger ->debug ('No status reports found for authenticator. ' , [
261+ 'aaguid ' => $ aaguid ,
262+ ]);
263+ return ;
264+ }
265+
266+ $ lastStatusReport = end ($ statusReports );
267+ $ this ->logger ->debug ('Status report found. ' , [
268+ 'aaguid ' => $ aaguid ,
269+ 'status ' => $ lastStatusReport ->status ,
270+ ]);
271+
272+ if ($ lastStatusReport ->isCompromised ()) {
273+ $ this ->logger ->warning ('Authenticator is marked as compromised in MDS. ' , [
274+ 'aaguid ' => $ aaguid ,
275+ 'status ' => $ lastStatusReport ->status ,
276+ ]);
277+ throw AuthenticatorResponseVerificationException::create (
278+ 'The authenticator is compromised and cannot be used '
279+ );
179280 }
180281 }
181282
182283 private function checkCertificateChain (
183284 AttestationStatement $ attestationStatement ,
184- ? MetadataStatement $ metadataStatement
285+ MetadataStatement $ metadataStatement
185286 ): void {
186287 $ trustPath = $ attestationStatement ->trustPath ;
187- if (! $ trustPath instanceof CertificateTrustPath) {
188- return ;
189- }
288+ $ trustPath instanceof CertificateTrustPath || throw AuthenticatorResponseVerificationException:: create (
289+ ' Certificate trust path is required for attestation verification '
290+ );
190291 $ authenticatorCertificates = $ trustPath ->certificates ;
191- if ($ metadataStatement === null ) {
192- $ this ->certificateChainValidator ?->check($ authenticatorCertificates , []);
193- return ;
194- }
195292 $ trustedCertificates = CertificateToolbox::fixPEMStructures (
196293 $ metadataStatement ->attestationRootCertificates
197294 );
0 commit comments