[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-85 - Allow Multiple Trust Anchors

Codeberg noreply at shibboleth.net
Mon Oct 12 15:21:16 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository java-idp-plugin-webauthn.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-webauthn/commit/a065d7ab1a547689d68e96fb146e5d2809e75c6a

The following commit(s) were added to refs/heads/main by this push:
     new a065d7a  JWEBAUTHN-85 - Allow Multiple Trust Anchors
a065d7a is described below

commit a065d7ab1a547689d68e96fb146e5d2809e75c6a
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Oct 12 17:19:48 2026 +0200

    JWEBAUTHN-85 - Allow Multiple Trust Anchors
    
      - Support multiple trust root files and hence multiple trust anchors
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-85
---
 .../metadata/impl/FidoMetadataServiceFactory.java  | 101 +++++++++++++++++++--
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   3 +-
 .../conf/authn/webauthn-metadata.properties        |   2 +
 3 files changed, 95 insertions(+), 11 deletions(-)

diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java
index ebb459c..6622093 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java
@@ -21,9 +21,15 @@ import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
 import java.security.cert.CRL;
 import java.security.cert.CRLException;
+import java.security.cert.CertificateException;
+import java.security.cert.TrustAnchor;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -41,6 +47,7 @@ import com.yubico.fido.metadata.FidoMetadataService;
 
 import net.shibboleth.shared.annotation.constraint.Live;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -60,8 +67,8 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(FidoMetadataServiceFactory.class);
     
-    /** The file location of the trust root that verifies the downloaded metadata's signature.*/
-    @GuardedBy("this") @NonnullAfterInit private Resource trustRootFile;
+    /** The locations of the trust roots that verify the downloaded metadata's signature.*/
+    @GuardedBy("this") @Nonnull @NotLive private List<Resource> trustRootFiles;
     
     /** Where to cache the metadata blob.*/
     @GuardedBy("this") @Nullable private Resource cacheFile;
@@ -88,6 +95,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
     /** Constructor. */
     public FidoMetadataServiceFactory() {
         crls = CollectionSupport.emptyList();
+        trustRootFiles = CollectionSupport.emptyList();
         verifyDownloadOnly = false;
     }
 
@@ -105,7 +113,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
                     localMetadataBlobFile.getFilename());
             downloader = FidoMetadataDownloader.builder()
                 .expectLegalHeader(getExpectedLegalHeaders())
-                .useTrustRoot(X509Support.decodeCertificate(getTrustRootFile().getFile()))
+                .useTrustRoots(convertTrustRoots(getTrustRootFiles()))
                 .useBlob(loadMetadataJwt(localMetadataBlobFile))
                 .useCrls(loadCrls())
                 .verifyDownloadsOnly(verifyDownloadOnly)
@@ -114,7 +122,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
             log.debug("{}: Loading FIDO metadata blob from '{}'", getId(), metadataBlobUrl);
             downloader = FidoMetadataDownloader.builder()
                 .expectLegalHeader(getExpectedLegalHeaders())
-                .useTrustRoot(X509Support.decodeCertificate(getTrustRootFile().getFile()))
+                .useTrustRoots(convertTrustRoots(getTrustRootFiles()))
                 .downloadBlob(localMetadataBlobUrl.getURL())
                 .useBlobCacheFile(localMetadataCacheFile.getFile())
                 .useCrls(loadCrls())
@@ -135,6 +143,40 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
             throw new FatalBeanException("Can not construct FIDO Metadata service", e);
         }
     }
+    
+    /**
+     * Convert the list of trust root files into Java {@link TrustAnchor}s. Any that fail to be converted throw
+     * a fatal bean exception. 
+     * 
+     * @param trustRootFiles the list of files to use as trust roots
+     * 
+     * @return a set of converted {@link TrustAnchor}s. 
+     * 
+     * @throws FatalBeanException if a trust root file does not exist or cannot be converted to 
+     * 								a {@link TrustAnchor}
+     * 
+     * @since 1.4.3
+     */
+    private Set<TrustAnchor> convertTrustRoots(@Nonnull final List<Resource> trustRootFiles) {    	
+    	final Set<TrustAnchor> anchors = new HashSet<>(trustRootFiles.size());
+    	for (final Resource file : trustRootFiles) {
+    		if (file == null) {
+    			throw new FatalBeanException("Trust anchor file was null");
+    		}
+    		if (file.exists()) {
+    			try {
+					anchors.add(new TrustAnchor(X509Support.decodeCertificate(file.getFile()),null));
+				} catch (CertificateException | IOException e) {
+					throw new FatalBeanException("Unable to decode file '" + file.getFilename() + 
+							"' to X509 certificate", e);
+				}
+    		} else {
+    			throw new FatalBeanException("Unable to convert file '" + file.getFilename() + 
+    					"' to trust anchor: file does not exist");
+    		}
+    	}
+    	return anchors;
+    }
 
     
     /**
@@ -178,10 +220,12 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
 
     @Override protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
-
-        if (trustRootFile ==  null) {
-            throw new ComponentInitializationException("trustRootFile cannot be null");
+        
+        if (trustRootFiles.isEmpty()) {
+            throw new ComponentInitializationException("At least one Trust Root file is required to verify the "
+            		+ "metadata blob");
         }
+
         if (metadataBlobUrl ==  null && metadataBlobFile == null) {
             throw new ComponentInitializationException("Metadata blob URL or file must be set");
         }
@@ -216,9 +260,12 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
      * 
      * @param file the trust root file
      */
-    public synchronized void setTrustRootFile(@Nonnull final Resource file) {
+    public synchronized void setTrustRootFile(@Nullable final Resource file) {
         checkSetterPreconditions();
-        trustRootFile = Constraint.isNotNull(file, "trustRootCacheFile can not be null");
+        // The file can now be null
+        if (file != null) {
+        	setTrustRootFiles(CollectionSupport.listOf(file));
+        }
     }
     
     /**
@@ -228,7 +275,41 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
      */
     @NonnullAfterInit private synchronized Resource getTrustRootFile() {
         checkComponentActive();
-        return trustRootFile;
+        return trustRootFiles.get(0);
+    }
+    
+    /**
+     * Get the trust root files.
+     * 
+     * @return the list of trust root files.
+     * 
+     * @since 1.4.3
+     */
+    @Nonnull @NotLive public synchronized List<Resource> getTrustRootFiles() {
+    	checkComponentActive();
+		return trustRootFiles;
+	}
+    
+    /**
+     * Set the trust roots.
+     * 
+     * @param roots the trust roots
+     * 
+     * @since 1.4.3
+     */
+    public synchronized void setTrustRootFiles(@Nullable final List<Resource> roots) {
+        checkSetterPreconditions();
+
+        if (roots != null) {
+    		trustRootFiles = CollectionSupport.copyToList(roots);    	  
+	    	if (log.isTraceEnabled()) {
+	        	log.trace("Setting '{}' trust root file(s): '{}'",trustRootFiles.size(), 
+	        			trustRootFiles.stream().filter(Objects::nonNull)
+	        		.map(Resource::getDescription)
+	        		.collect(Collectors.joining(", ")));
+	    	}
+        }
+
     }
     
     /**
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 3cde879..8840acd 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -117,7 +117,8 @@
     <bean id="shibboleth.authn.WebAuthn.DefaultWebAuthnFidoMetadataServiceFactory" 
         class="net.shibboleth.idp.plugin.authn.webauthn.metadata.impl.FidoMetadataServiceFactory" scope="singleton"
         lazy-init="true"
-        p:trustRootFile="%{idp.authn.webauthn.metadata.trustRootFile:}"
+        p:trustRootFile="%{idp.authn.webauthn.metadata.trustRootFile:#{null}}"
+        p:trustRootFiles="%{idp.authn.webauthn.metadata.trustRootFiles:#{null}}"
         p:cacheFile="%{idp.authn.webauthn.metadata.cacheFile:}"
         p:crls="%{idp.authn.webauthn.metadata.crls:#{null}}"
         p:metadataBlobUrl="%{idp.authn.webauthn.metadata.metadataBlobUrl:https://mds3.fidoalliance.org}"
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-metadata.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-metadata.properties
index c2f8d94..d42488e 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-metadata.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-metadata.properties
@@ -1,6 +1,8 @@
 # Enable FIDO Alliance metadata? If you want to use the metadata service, you *must* download and set the trust root and the legal blob file headers
 #idp.authn.webauthn.metadata.enabled = false
 #idp.authn.webauthn.metadata.trustRootFile = %{idp.home}/credentials/root-r3.crt
+# A list of trust root files, space separated. Can not be used with the single trustRootFile property.
+#idp.authn.webauthn.metadata.trustRootFiles = %{idp.home}/credentials/root-r3.crt
 #idp.authn.webauthn.metadata.expectedLegalHeaders = Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/
 # Downloaded CRLs to check metadata signature revocation status.
 #idp.authn.webauthn.metadata.crls = %{idp.home}/credentials/root-r3.crl

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list