From cbb3c6a606f6303edc152ad7f20e7b8f089d0884 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 15 Sep 2026 12:26:59 +0200 Subject: [PATCH 1/2] Fall back to any trust root if both x5u and x5c are missing --- ...idoMetadataDownloaderIntegrationTest.scala | 1 + .../fido/metadata/FidoMetadataDownloader.java | 52 ++++++++++++++----- .../metadata/FidoMetadataDownloaderSpec.scala | 13 ++++- 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala index 7e67524b2..0249bf6cf 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala @@ -60,6 +60,7 @@ class FidoMetadataDownloaderIntegrationTest .getBlob .getHeader, ) + .get ) .asScala :+ trustRootCert for { cert <- certChain } { diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index 85c93b27e..73c0daac7 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -1351,7 +1351,42 @@ private MetadataBLOB verifyBlob(ParseResult parseResult, Set trustA InvalidAlgorithmParameterException, FidoMetadataDownloaderException { final MetadataBLOBHeader header = parseResult.blob.getHeader(); - final List certChain = fetchHeaderCertChain(trustAnchors, header); + final Optional> certChain = fetchHeaderCertChain(trustAnchors, header); + if (certChain.isPresent()) { + return tryVerifyBlob(parseResult, trustAnchors, certChain.get()); + } else { + log.debug( + "x5u and x5c both missing from BLOB header. Falling back to using trust anchors as BLOB signer."); + for (TrustAnchor ta : trustAnchors) { + final X509Certificate cert = ta.getTrustedCert(); + if (cert != null) { + try { + return tryVerifyBlob(parseResult, trustAnchors, Collections.singletonList(cert)); + } catch (FidoMetadataDownloaderException e) { + if (e.getReason() == Reason.BAD_SIGNATURE) { + log.debug("Failed to verify BLOB with trust anchor: {}", ta); + } else { + throw e; + } + } catch (SignatureException | CertPathValidatorException e) { + log.debug("Failed to verify BLOB with trust anchor: {}", ta); + } + } + } + throw new IllegalArgumentException("Failed to verify BLOB with any trust anchor."); + } + } + + private MetadataBLOB tryVerifyBlob( + ParseResult parseResult, Set trustAnchors, List certChain) + throws CertificateException, + NoSuchAlgorithmException, + InvalidKeyException, + SignatureException, + CertPathValidatorException, + InvalidAlgorithmParameterException, + FidoMetadataDownloaderException { + final MetadataBLOBHeader header = parseResult.blob.getHeader(); final X509Certificate leafCert = certChain.get(0); final Signature signature; @@ -1470,7 +1505,7 @@ static class ParseResult { } /** Parse the header cert chain and download any certificates as necessary. */ - List fetchHeaderCertChain( + Optional> fetchHeaderCertChain( Set trustAnchors, MetadataBLOBHeader header) throws IOException, CertificateException { if (header.getX5u().isPresent()) { @@ -1492,18 +1527,11 @@ List fetchHeaderCertChain( X509Certificate x509Certificate = CertificateParser.parsePem(pem); certs.add(x509Certificate); } - return certs; + return Optional.of(certs); } else if (header.getX5c().isPresent()) { - return header.getX5c().get(); + return Optional.of(header.getX5c().get()); } else { - return trustAnchors.stream() - .map(TrustAnchor::getTrustedCert) - .findFirst() - .map(Collections::singletonList) - .orElseThrow( - () -> - new IllegalArgumentException( - "x5u and x5c both missing from BLOB header, and no given trust anchor could be interpreted as an X509Certificate.")); + return Optional.empty(); } } diff --git a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala index 436a43701..ba62ff03c 100644 --- a/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala +++ b/webauthn-server-attestation/src/test/scala/com/yubico/fido/metadata/FidoMetadataDownloaderSpec.scala @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.yubico.fido.metadata.FidoMetadataDownloader.CachePolicyDecision import com.yubico.fido.metadata.FidoMetadataDownloader.FidoMetadataDownloaderBuilder import com.yubico.fido.metadata.FidoMetadataDownloader.TrustRootsCacheValue +import com.yubico.fido.metadata.FidoMetadataDownloader.importTrustAnchor import com.yubico.fido.metadata.FidoMetadataDownloaderException.Reason import com.yubico.internal.util.BinaryUtil import com.yubico.internal.util.JacksonCodecs @@ -1958,7 +1959,13 @@ class FidoMetadataDownloaderSpec } it("Missing x5c means the trust root cert is used as the signer.") { - val (trustRootCert, caKeypair, caName) = makeTrustRootCert() + val (trustRootCert, caKeypair, caName) = + makeTrustRootCert(distinguishedName = + "CN=Yubico java-webauthn-server unit tests CA 1, O=Yubico" + ) + val (trustRootCert0, _, _) = makeTrustRootCert(distinguishedName = + "CN=Yubico java-webauthn-server unit tests CA 0, O=Yubico" + ) val blobJwt = makeBlob( caKeypair, @@ -1987,7 +1994,9 @@ class FidoMetadataDownloaderSpec .expectLegalHeader( "Kom ihåg att du aldrig får snyta dig i mattan!" ) - .useTrustRoot(trustRootCert) + .useTrustRoots( + Set(trustRootCert0, trustRootCert).map(importTrustAnchor).asJava + ) .useBlob(blobJwt) .useCrls(crls.asJava) .clock(Clock.fixed(CertValidFrom, ZoneOffset.UTC)) From dbc1857da45eef7c57cf26a7f91c6cbd22643507 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 15 Sep 2026 12:53:14 +0200 Subject: [PATCH 2/2] Delete unused parameter --- .../metadata/FidoMetadataDownloaderIntegrationTest.scala | 6 +----- .../com/yubico/fido/metadata/FidoMetadataDownloader.java | 5 ++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala index 0249bf6cf..888647a6f 100644 --- a/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala +++ b/webauthn-server-attestation/src/integrationTest/scala/com/yubico/fido/metadata/FidoMetadataDownloaderIntegrationTest.scala @@ -12,7 +12,6 @@ import org.scalatest.tags.Network import org.scalatest.tags.Slow import org.scalatestplus.junit.JUnitRunner -import java.util.Collections import scala.jdk.CollectionConverters.ListHasAsScala @Slow @@ -52,13 +51,10 @@ class FidoMetadataDownloaderIntegrationTest .cacheSynchronized( downloader .fetchHeaderCertChain( - Collections.singleton( - FidoMetadataDownloader.importTrustAnchor(trustRootCert) - ), downloader .parseBlob(TestCaches.blobCache.get.getBytes) .getBlob - .getHeader, + .getHeader ) .get ) diff --git a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java index 73c0daac7..e11978346 100644 --- a/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java +++ b/webauthn-server-attestation/src/main/java/com/yubico/fido/metadata/FidoMetadataDownloader.java @@ -1351,7 +1351,7 @@ private MetadataBLOB verifyBlob(ParseResult parseResult, Set trustA InvalidAlgorithmParameterException, FidoMetadataDownloaderException { final MetadataBLOBHeader header = parseResult.blob.getHeader(); - final Optional> certChain = fetchHeaderCertChain(trustAnchors, header); + final Optional> certChain = fetchHeaderCertChain(header); if (certChain.isPresent()) { return tryVerifyBlob(parseResult, trustAnchors, certChain.get()); } else { @@ -1505,8 +1505,7 @@ static class ParseResult { } /** Parse the header cert chain and download any certificates as necessary. */ - Optional> fetchHeaderCertChain( - Set trustAnchors, MetadataBLOBHeader header) + Optional> fetchHeaderCertChain(MetadataBLOBHeader header) throws IOException, CertificateException { if (header.getX5u().isPresent()) { final URL x5u = header.getX5u().get();