+ * GitHub Issue #14602 + */ +@Path(Constants.TRUST) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class Fido2TrustResource extends BaseResource { + + private static final String ERR_MSG = "Exception while getting Fido2 trust data is - "; + + @Inject + Logger logger; + + @Inject + Fido2TrustService fido2TrustService; + + /** + * Retrieve the attestation policy the FIDO2 server is actually applying. + * + * @return a Response containing a JsonNode with the effective attestation configuration + */ + @Operation(summary = "Get effective Fido2 attestation configuration.", description = "Get effective Fido2 attestation configuration.", operationId = "get-fido2-trust-attestation-config", tags = { + "Fido2 - Trust" }, security = { + @SecurityRequirement(name = "oauth2", scopes = { Constants.FIDO2_CONFIG_READ_ACCESS }), + @SecurityRequirement(name = "oauth2", scopes = { Constants.FIDO2_CONFIG_WRITE_ACCESS }), + @SecurityRequirement(name = "oauth2", scopes = { Constants.FIDO2_ADMIN_ACCESS }), + @SecurityRequirement(name = "oauth2", scopes = { ApiAccessConstants.SUPER_ADMIN_READ_ACCESS }) }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Ok", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = JsonNode.class), examples = @ExampleObject(name = "Response example", value = "example/fido2/trust/fido2-attestation-config.json"))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "500", description = "InternalServerError") }) + @GET + @Path(Constants.ATTESTATION_CONFIG) + @ProtectedApi(scopes = { Constants.FIDO2_CONFIG_READ_ACCESS }, groupScopes = { + Constants.FIDO2_CONFIG_WRITE_ACCESS }, superScopes = { Constants.FIDO2_ADMIN_ACCESS, + ApiAccessConstants.SUPER_ADMIN_READ_ACCESS }) + public Response getAttestationConfig() { + logger.info("Fido2 attestation trust configuration"); + JsonNode jsonNode = null; + try { + jsonNode = fido2TrustService.getAttestationConfig(null); + + if (logger.isDebugEnabled()) { + logger.debug("Fido2 attestation trust configuration - jsonNode:{}", jsonNode); + } + + } catch (Exception ex) { + logger.error(ERR_MSG, ex); + throwInternalServerException(ex); + } + return Response.ok(jsonNode).build(); + } + +} diff --git a/jans-config-api/plugins/fido2-plugin/src/main/java/io/jans/configapi/plugin/fido2/service/Fido2TrustService.java b/jans-config-api/plugins/fido2-plugin/src/main/java/io/jans/configapi/plugin/fido2/service/Fido2TrustService.java new file mode 100644 index 00000000000..9500e1b9d50 --- /dev/null +++ b/jans-config-api/plugins/fido2-plugin/src/main/java/io/jans/configapi/plugin/fido2/service/Fido2TrustService.java @@ -0,0 +1,88 @@ +/* + * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.configapi.plugin.fido2.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; + +import io.jans.configapi.plugin.fido2.util.Constants; +import io.jans.configapi.plugin.fido2.util.Fido2Util; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Proxies the read-only attestation and MDS diagnostics exposed by the FIDO2 server, so the Admin UI + * can render an attestation and MDS status panel through the Config API. + *
+ * GitHub Issue #14602
+ */
+@Singleton
+public class Fido2TrustService {
+
+ private static final String CONTENT_TYPE = "Content-Type";
+ private static final String AUTHORIZATION = "Authorization";
+ private static final String FIDO2_TRUST_BASE_URL = "/jans-fido2/restv1/trust";
+
+ @Inject
+ Logger log;
+
+ @Inject
+ Fido2Util fido2Util;
+
+ public String getFido2TrustUrl() {
+ return fido2Util.getIssuer() + FIDO2_TRUST_BASE_URL;
+ }
+
+ public String getAttestationConfigUrl() {
+ return getFido2TrustUrl() + Constants.ATTESTATION_CONFIG;
+ }
+
+ /**
+ * The attestation policy the FIDO2 server is applying.
+ *
+ * @param token bearer token to forward, may be null
+ * @return attestation trust configuration
+ */
+ public JsonNode getAttestationConfig(String token) throws JsonProcessingException {
+ log.info("Get Fido2 attestation trust configuration");
+
+ return getTrustData(this.getAttestationConfigUrl(), buildHeaders(token));
+ }
+
+ private JsonNode getTrustData(String url, Map
+ * This service only reads state that the attestation service already holds; it never changes
+ * attestation behaviour.
+ *
+ * @author Janssen Project
+ */
+@ApplicationScoped
+public class TrustStatusService {
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private AppConfiguration appConfiguration;
+
+ @Inject
+ private AttestationCertificateService attestationCertificateService;
+
+ /**
+ * The attestation policy the server is actually applying.
+ */
+ public AttestationTrustConfig getAttestationTrustConfig() {
+ AttestationTrustConfig trustConfig = new AttestationTrustConfig();
+ Fido2Configuration fido2Configuration = appConfiguration.getFido2Configuration();
+ if (fido2Configuration == null) {
+ log.warn("Fido2 configuration is not available; reporting empty attestation trust config");
+ return trustConfig;
+ }
+
+ String configuredMode = fido2Configuration.getAttestationMode();
+ trustConfig.setAttestationMode(configuredMode);
+ trustConfig.setAttestationModeRecognized(AttestationMode.getByValue(configuredMode) != null);
+ // Only "enforced" applies the stricter MDS trust rules (AttestationCertificateService
+ // .isAttestationEnforced) — "disabled" and "monitor" both stay lenient, so anything other than
+ // enforced still accepts an authenticator that fails attestation validation.
+ trustConfig.setUnattestedAuthenticatorsAllowed(!isAttestationEnforced(configuredMode));
+ trustConfig.setEnterpriseAttestation(fido2Configuration.isEnterpriseAttestation());
+ trustConfig.setMetadataServiceDisabled(fido2Configuration.isDisableMetadataService());
+ trustConfig.setAppleRootCaPresent(attestationCertificateService.isAppleRootCaPresent());
+ trustConfig.setEnabledFidoAlgorithms(fido2Configuration.getEnabledFidoAlgorithms());
+ trustConfig.setHints(fido2Configuration.getHints());
+
+ return trustConfig;
+ }
+
+ private boolean isAttestationEnforced(String configuredMode) {
+ return AttestationMode.ENFORCED.getValue().equalsIgnoreCase(configuredMode);
+ }
+}
diff --git a/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2TrustController.java b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2TrustController.java
new file mode 100644
index 00000000000..1ab205e7534
--- /dev/null
+++ b/jans-fido2/server/src/main/java/io/jans/fido2/ws/rs/controller/Fido2TrustController.java
@@ -0,0 +1,88 @@
+/*
+ * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
+ *
+ * Copyright (c) 2020, Janssen Project
+ */
+
+package io.jans.fido2.ws.rs.controller;
+
+import org.slf4j.Logger;
+
+import io.jans.fido2.model.error.ErrorResponseFactory;
+import io.jans.fido2.service.DataMapperService;
+import io.jans.fido2.service.trust.TrustStatusService;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+/**
+ * REST API controller exposing attestation trust diagnostics.
+ *
+ * Read-only: these endpoints surface configuration that already exists in the server, so an
+ * administrator can see why enrollments are failing instead of that appearing to end users as a
+ * generic registration failure. Nothing here changes attestation behaviour.
+ *
+ * SECURITY NOTE: as with the metrics endpoints, authentication and authorization are expected to be
+ * enforced at the infrastructure level (API gateway, OAuth interceptor, or reverse proxy) or via the
+ * Config API fido2 plugin, which applies the fido2 configuration scopes.
+ *
+ * @author Janssen Project
+ */
+@ApplicationScoped
+@Path("/trust")
+public class Fido2TrustController {
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private TrustStatusService trustStatusService;
+
+ @Inject
+ private DataMapperService dataMapperService;
+
+ @Inject
+ private ErrorResponseFactory errorResponseFactory;
+
+ /**
+ * Effective attestation configuration: the mode in force, whether unattested authenticators are
+ * still accepted, and whether the trust anchors attestation depends on are present.
+ *
+ * @return attestation trust configuration
+ */
+ @GET
+ @Path("/attestation/config")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getAttestationConfig() {
+ return processRequest(
+ () -> Response.ok(dataMapperService.writeValueAsString(trustStatusService.getAttestationTrustConfig()))
+ .build());
+ }
+
+ private Response processRequest(RequestProcessor processor) {
+ try {
+ return processor.process();
+ } catch (WebApplicationException e) {
+ // Already carries the right status code.
+ throw e;
+ } catch (Exception e) {
+ // Log the detail, return a generic message — the response must not leak internal paths or
+ // connection details.
+ log.error("Error processing trust status request: {}", e.getMessage(), e);
+ throw errorResponseFactory.unknownError("Failed to read trust status");
+ }
+ }
+
+ /**
+ * Functional interface for request processing; throws Exception to accommodate the checked
+ * exceptions raised while serializing the response.
+ */
+ private interface RequestProcessor {
+ Response process() throws Exception;
+ }
+}
diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/mds/AttestationCertificateServiceTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/mds/AttestationCertificateServiceTest.java
index dfdf67169db..302b884b8e6 100644
--- a/jans-fido2/server/src/test/java/io/jans/fido2/service/mds/AttestationCertificateServiceTest.java
+++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/mds/AttestationCertificateServiceTest.java
@@ -16,12 +16,16 @@
import org.mockito.quality.Strictness;
import org.slf4j.Logger;
+import javax.security.auth.x500.X500Principal;
+
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -82,4 +86,44 @@ void getAttestationRootCertificates_monitorAndMetadataFetchFails_fallsBack() {
// Regression guard: monitor/disabled keep the previous lenient behavior — fall back, do not throw.
assertDoesNotThrow(() -> attestationCertificateService.getAttestationRootCertificates(authData, certs));
}
+
+ // #14602 — Apple root CA presence surfaced by the trust/attestation/config endpoint.
+
+ private void configureAuthenticatorCertsFolder() {
+ Fido2Configuration cfg = mock(Fido2Configuration.class);
+ when(cfg.getAuthenticatorCertsFolder()).thenReturn("/etc/jans/conf/fido2/authenticator_cert");
+ when(appConfiguration.getFido2Configuration()).thenReturn(cfg);
+ }
+
+ @Test
+ void isAppleRootCaPresent_ifCertificateLoaded_returnsTrue() {
+ configureAuthenticatorCertsFolder();
+ X509Certificate appleCert = mock(X509Certificate.class);
+ when(appleCert.getSubjectX500Principal()).thenReturn(new X500Principal("CN=Apple WebAuthn Root CA"));
+ when(certificateService.getCertificateByDisplayName("Apple_WebAuthn_Root_CA.pem")).thenReturn(appleCert);
+
+ attestationCertificateService.init(new Object());
+
+ assertTrue(attestationCertificateService.isAppleRootCaPresent());
+ }
+
+ @Test
+ void isAppleRootCaPresent_ifCertificateMissing_returnsFalse() {
+ configureAuthenticatorCertsFolder();
+ when(certificateService.getCertificateByDisplayName("Apple_WebAuthn_Root_CA.pem")).thenReturn(null);
+
+ attestationCertificateService.init(new Object());
+
+ // Today this condition is only a startup log warning; the endpoint has to be able to report it.
+ assertFalse(attestationCertificateService.isAppleRootCaPresent());
+ }
+
+ @Test
+ void isAppleRootCaPresent_ifFido2ConfigurationMissing_returnsFalse() {
+ when(appConfiguration.getFido2Configuration()).thenReturn(null);
+
+ attestationCertificateService.init(new Object());
+
+ assertFalse(attestationCertificateService.isAppleRootCaPresent());
+ }
}
diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/service/trust/TrustStatusServiceTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/service/trust/TrustStatusServiceTest.java
new file mode 100644
index 00000000000..5e9cbe2e035
--- /dev/null
+++ b/jans-fido2/server/src/test/java/io/jans/fido2/service/trust/TrustStatusServiceTest.java
@@ -0,0 +1,120 @@
+/*
+ * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
+ *
+ * Copyright (c) 2020, Janssen Project
+ */
+
+package io.jans.fido2.service.trust;
+
+import io.jans.fido2.model.conf.AppConfiguration;
+import io.jans.fido2.model.conf.Fido2Configuration;
+import io.jans.fido2.model.trust.AttestationTrustConfig;
+import io.jans.fido2.service.mds.AttestationCertificateService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.slf4j.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class TrustStatusServiceTest {
+
+ @InjectMocks
+ private TrustStatusService trustStatusService;
+
+ @Mock
+ private Logger log;
+ @Mock
+ private AppConfiguration appConfiguration;
+ @Mock
+ private AttestationCertificateService attestationCertificateService;
+
+ private Fido2Configuration configure(String attestationMode, boolean metadataServiceDisabled) {
+ Fido2Configuration cfg = mock(Fido2Configuration.class);
+ when(cfg.getAttestationMode()).thenReturn(attestationMode);
+ when(cfg.isDisableMetadataService()).thenReturn(metadataServiceDisabled);
+ when(appConfiguration.getFido2Configuration()).thenReturn(cfg);
+ return cfg;
+ }
+
+ @Test
+ void getAttestationTrustConfig_ifEnforced_disallowsUnattested() {
+ configure("enforced", false);
+ when(attestationCertificateService.isAppleRootCaPresent()).thenReturn(true);
+
+ AttestationTrustConfig config = trustStatusService.getAttestationTrustConfig();
+
+ assertEquals("enforced", config.getAttestationMode());
+ assertTrue(config.isAttestationModeRecognized());
+ assertFalse(config.isUnattestedAuthenticatorsAllowed());
+ assertTrue(config.isAppleRootCaPresent());
+ }
+
+ @Test
+ void getAttestationTrustConfig_ifMonitor_stillAllowsUnattested() {
+ configure("monitor", false);
+
+ AttestationTrustConfig config = trustStatusService.getAttestationTrustConfig();
+
+ // Only "enforced" applies the stricter trust rules — monitor is lenient, which is exactly the
+ // thing administrators get wrong about the default.
+ assertTrue(config.isAttestationModeRecognized());
+ assertTrue(config.isUnattestedAuthenticatorsAllowed());
+ }
+
+ @Test
+ void getAttestationTrustConfig_ifDisabled_allowsUnattested() {
+ configure("disabled", true);
+
+ AttestationTrustConfig config = trustStatusService.getAttestationTrustConfig();
+
+ assertTrue(config.isAttestationModeRecognized());
+ assertTrue(config.isUnattestedAuthenticatorsAllowed());
+ assertTrue(config.isMetadataServiceDisabled());
+ }
+
+ @Test
+ void getAttestationTrustConfig_ifModeUnrecognized_reportsRawValueAndLenientBehaviour() {
+ configure("Enforce", false);
+
+ AttestationTrustConfig config = trustStatusService.getAttestationTrustConfig();
+
+ // A typo silently makes the server lenient; report it rather than normalising it away.
+ assertEquals("Enforce", config.getAttestationMode());
+ assertFalse(config.isAttestationModeRecognized());
+ assertTrue(config.isUnattestedAuthenticatorsAllowed());
+ }
+
+ @Test
+ void getAttestationTrustConfig_ifAppleRootCaMissing_reportsAbsent() {
+ configure("monitor", false);
+ when(attestationCertificateService.isAppleRootCaPresent()).thenReturn(false);
+
+ AttestationTrustConfig config = trustStatusService.getAttestationTrustConfig();
+
+ // Today this condition is only a startup log warning.
+ assertFalse(config.isAppleRootCaPresent());
+ }
+
+ @Test
+ void getAttestationTrustConfig_ifNoFido2Configuration_returnsEmptyConfig() {
+ when(appConfiguration.getFido2Configuration()).thenReturn(null);
+
+ AttestationTrustConfig config = trustStatusService.getAttestationTrustConfig();
+
+ assertNotNull(config);
+ assertNull(config.getAttestationMode());
+ }
+}
diff --git a/jans-fido2/server/src/test/java/io/jans/fido2/ws/rs/controller/Fido2TrustControllerTest.java b/jans-fido2/server/src/test/java/io/jans/fido2/ws/rs/controller/Fido2TrustControllerTest.java
new file mode 100644
index 00000000000..f19716c0cf1
--- /dev/null
+++ b/jans-fido2/server/src/test/java/io/jans/fido2/ws/rs/controller/Fido2TrustControllerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
+ *
+ * Copyright (c) 2020, Janssen Project
+ */
+
+package io.jans.fido2.ws.rs.controller;
+
+import io.jans.fido2.model.error.ErrorResponseFactory;
+import io.jans.fido2.model.trust.AttestationTrustConfig;
+import io.jans.fido2.service.DataMapperService;
+import io.jans.fido2.service.trust.TrustStatusService;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.Response;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.slf4j.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class Fido2TrustControllerTest {
+
+ @InjectMocks
+ private Fido2TrustController fido2TrustController;
+
+ @Mock
+ private Logger log;
+ @Mock
+ private TrustStatusService trustStatusService;
+ @Mock
+ private DataMapperService dataMapperService;
+ @Mock
+ private ErrorResponseFactory errorResponseFactory;
+
+ @Test
+ void getAttestationConfig_returnsOk() throws Exception {
+ when(trustStatusService.getAttestationTrustConfig()).thenReturn(new AttestationTrustConfig());
+ when(dataMapperService.writeValueAsString(any())).thenReturn("{}");
+
+ Response response = fido2TrustController.getAttestationConfig();
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ }
+
+ @Test
+ void getAttestationConfig_ifServiceFails_raisesSanitizedError() {
+ when(trustStatusService.getAttestationTrustConfig())
+ .thenThrow(new IllegalStateException("jdbc://secret-host/db"));
+ when(errorResponseFactory.unknownError(anyString()))
+ .thenReturn(new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR));
+
+ assertThrows(WebApplicationException.class, () -> fido2TrustController.getAttestationConfig());
+
+ // The internal detail goes to the log, never into the response.
+ verify(errorResponseFactory).unknownError("Failed to read trust status");
+ }
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 85a14fabcd7..f2ef5bd996a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -223,6 +223,7 @@ nav:
- janssen-server/fido/README.md
- Properties: janssen-server/fido/fido2-server-properties-config.md
- Vendor Metadata: janssen-server/fido/vendor-metadata.md
+ - Trust Diagnostics: janssen-server/fido/trust-diagnostics.md
- Conditional UI and Fallback: janssen-server/fido/conditional-ui-and-fallback.md
- Passkey Telemetry: janssen-server/fido/passkey-telemetry.md
- SAML: