From 94ee6d81fb0233a4c693374484166dc3d0e11dc1 Mon Sep 17 00:00:00 2001 From: Palpable <110998749+Vitalisn4@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:46:17 +0100 Subject: [PATCH] [OID4VCI] Realign naming of attribute configuring algorithms for credential (#44765) Closes #44621 Signed-off-by: Vitalisn4 Signed-off-by: mposolda Signed-off-by: Ingrid Kamga Co-authored-by: Marek Posolda Co-authored-by: Ingrid Kamga --- .../oid4vci/vc-issuer-configuration.adoc | 8 +-- .../models/oid4vci/CredentialScopeModel.java | 21 +++----- .../OID4VCIssuerWellKnownProvider.java | 17 ++---- .../SupportedCredentialConfiguration.java | 13 +++-- .../OID4VCIssuerWellKnownProviderTest.java | 54 ++++++++++++++----- 5 files changed, 60 insertions(+), 53 deletions(-) diff --git a/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc b/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc index 337d7f67017..60b32a4cabc 100644 --- a/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc +++ b/docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc @@ -248,7 +248,7 @@ Create a JSON file (e.g., `client-scopes.json`) with the following content: "vc.verifiable_credential_type": "my-vct", "vc.supported_credential_types": "credential-type-1,credential-type-2", "vc.credential_contexts": "context-1,context-2", - "vc.proof_signing_alg_values_supported": "ES256", + "vc.credential_signing_alg": "ES256", "vc.cryptographic_binding_methods_supported": "jwk", "vc.signing_key_id": "key-id-123456", "vc.display": "[{\"name\": \"IdentityCredential\", \"logo\": {\"uri\": \"https://university.example.edu/public/logo.png\", \"alt_text\": \"a square logo of a university\"}, \"locale\": \"en-US\", \"background_color\": \"#12107c\", \"text_color\": \"#FFFFFF\"}]", @@ -358,10 +358,10 @@ _Default_: `$\{name}+` | The context values of the Verifiable Credential Type. + _Default_: `$\{name}+` -| `vc.proof_signing_alg_values_supported` +| `vc.credential_signing_alg` | optional -| Supported signature algorithms for this credential. + -_Default_: All present keys supporting JWS algorithms in the realm. +| Supported signature algorithm for this credential. + +_Default_: All asymmetric signing algorithms backed by realm keys. | `vc.cryptographic_binding_methods_supported` | optional diff --git a/server-spi-private/src/main/java/org/keycloak/models/oid4vci/CredentialScopeModel.java b/server-spi-private/src/main/java/org/keycloak/models/oid4vci/CredentialScopeModel.java index 6fb210ecafe..ffb0b04faa5 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/oid4vci/CredentialScopeModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/oid4vci/CredentialScopeModel.java @@ -72,10 +72,9 @@ public class CredentialScopeModel implements ClientScopeModel { public static final String CONTEXTS = "vc.credential_contexts"; /** - * if the credential is only meant for specific signing algorithms the global default list can be overridden here. - * The global default list is retrieved from the available keys in the realm. + * The credential signature algorithm. If it is not configured, then the realm active key is used to sign the verifiable credential */ - public static final String SIGNING_ALG_VALUES_SUPPORTED = "vc.proof_signing_alg_values_supported"; + public static final String SIGNING_ALG = "vc.credential_signing_alg"; /** * if the credential is only meant for specific cryptographic binding algorithms the global default list can be @@ -269,20 +268,12 @@ public class CredentialScopeModel implements ClientScopeModel { clientScope.setAttribute(CONTEXTS, String.join(",", vcContexts)); } - public List getSigningAlgsSupported() { - return Optional.ofNullable(clientScope.getAttribute(SIGNING_ALG_VALUES_SUPPORTED)) - .map(s -> s.split(",")) - .map(Arrays::asList) - .orElse(Collections.emptyList()); + public String getSigningAlg() { + return clientScope.getAttribute(SIGNING_ALG); } - public void setSigningAlgsSupported(String signingAlgsSupported) { - clientScope.setAttribute(SIGNING_ALG_VALUES_SUPPORTED, signingAlgsSupported); - } - - public void setSigningAlgsSupported(List signingAlgsSupported) { - clientScope.setAttribute(SIGNING_ALG_VALUES_SUPPORTED, - String.join(",", signingAlgsSupported)); + public void setSigningAlg(String signingAlg) { + clientScope.setAttribute(SIGNING_ALG, signingAlg); } public List getCryptographicBindingMethods() { diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerWellKnownProvider.java index 615bf26584b..1259b864669 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerWellKnownProvider.java @@ -446,7 +446,7 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider { * and the credentials supported by the clients available in the session. */ public static Map getSupportedCredentials(KeycloakSession keycloakSession) { - List globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession); + List globalSupportedSigningAlgorithms = getSupportedAsymmetricSignatureAlgorithms(keycloakSession); RealmModel realm = keycloakSession.getContext().getRealm(); Map supportedCredentialConfigurations = @@ -466,7 +466,8 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider { public static SupportedCredentialConfiguration toSupportedCredentialConfiguration(KeycloakSession keycloakSession, CredentialScopeModel credentialModel) { - List globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession); + List globalSupportedSigningAlgorithms = getSupportedAsymmetricSignatureAlgorithms(keycloakSession); + return SupportedCredentialConfiguration.parse(keycloakSession, credentialModel, globalSupportedSigningAlgorithms); @@ -496,18 +497,6 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider { return getIssuer(context) + "/protocol/" + OID4VCLoginProtocolFactory.PROTOCOL_ID + "/" + OID4VCIssuerEndpoint.CREDENTIAL_PATH; } - public static List getSupportedSignatureAlgorithms(KeycloakSession session) { - RealmModel realm = session.getContext().getRealm(); - KeyManager keyManager = session.keys(); - - return keyManager.getKeysStream(realm) - .filter(key -> KeyUse.SIG.equals(key.getUse())) - .map(KeyWrapper::getAlgorithm) - .filter(algorithm -> algorithm != null && !algorithm.isEmpty()) - .distinct() - .collect(Collectors.toList()); - } - /** * Return the authorization servers from the issuer configuration. */ diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java index 776ab9b3da7..ad0866a96fd 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java @@ -23,11 +23,11 @@ import java.util.Optional; import org.keycloak.models.KeycloakSession; import org.keycloak.models.oid4vci.CredentialScopeModel; +import org.keycloak.utils.StringUtil; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.commons.collections4.ListUtils; /** * A supported credential, as used in the Credentials Issuer Metadata in OID4VCI @@ -118,13 +118,12 @@ public class SupportedCredentialConfiguration { ProofTypesSupported proofTypesSupported = ProofTypesSupported.parse(keycloakSession, keyAttestationsRequired, globalSupportedSigningAlgorithms); - credentialConfiguration.setProofTypesSupported(proofTypesSupported); + credentialConfiguration.setProofTypesSupported(proofTypesSupported); - List signingAlgsSupported = credentialScope.getSigningAlgsSupported(); - signingAlgsSupported = signingAlgsSupported.isEmpty() ? globalSupportedSigningAlgorithms : - // if the config has listed different algorithms than supported by keycloak we must use the - // intersection of the configuration with the actual supported algorithms. - ListUtils.intersection(signingAlgsSupported, globalSupportedSigningAlgorithms); + // Return single configured value for the signature algorithm if any + String signingAlgSupported = credentialScope.getSigningAlg(); + List signingAlgsSupported = StringUtil.isBlank(signingAlgSupported) ? globalSupportedSigningAlgorithms : + Collections.singletonList(signingAlgSupported); credentialConfiguration.setCredentialSigningAlgValuesSupported(signingAlgsSupported); // TODO resolve value dynamically from provider implementations? diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java index 757f0bab175..2adb7c42ff9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java @@ -61,6 +61,7 @@ import org.keycloak.protocol.oid4vc.model.DisplayObject; import org.keycloak.protocol.oid4vc.model.Format; import org.keycloak.protocol.oid4vc.model.JWTVCIssuerMetadata; import org.keycloak.protocol.oid4vc.model.KeyAttestationsRequired; +import org.keycloak.protocol.oid4vc.model.ProofType; import org.keycloak.protocol.oid4vc.model.ProofTypesSupported; import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration; import org.keycloak.representations.idm.ClientScopeRepresentation; @@ -585,9 +586,15 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest Matchers.containsInAnyOrder(credentialDefinitionTypes.toArray())); List signingAlgsSupported = new ArrayList<>(supportedConfig.getCredentialSigningAlgValuesSupported()); - String proofTypesSupportedString = supportedConfig.getProofTypesSupported().toJsonString(); + ProofTypesSupported proofTypesSupported = supportedConfig.getProofTypesSupported(); + String proofTypesSupportedString = proofTypesSupported.toJsonString(); - KeyAttestationsRequired expectedKeyAttestationsRequired = null; + MatcherAssert.assertThat(proofTypesSupported.getSupportedProofTypes().keySet(), + Matchers.containsInAnyOrder(ProofType.JWT, ProofType.ATTESTATION)); + + List expectedProofSigningAlgs = getAllAsymmetricAlgorithms(); + + KeyAttestationsRequired expectedKeyAttestationsRequired; if (Boolean.parseBoolean(clientScope.getAttributes().get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED))) { expectedKeyAttestationsRequired = new KeyAttestationsRequired(); expectedKeyAttestationsRequired.setKeyStorage( @@ -600,24 +607,37 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest .get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_USER_AUTH)) .map(s -> Arrays.asList(s.split(","))) .orElse(null)); + } else { + expectedKeyAttestationsRequired = null; } String expectedKeyAttestationsRequiredString = toJsonString(expectedKeyAttestationsRequired); + proofTypesSupported.getSupportedProofTypes().values() + .forEach(proofTypeData -> { + assertEquals(expectedKeyAttestationsRequired, proofTypeData.getKeyAttestationsRequired()); + MatcherAssert.assertThat(proofTypeData.getSigningAlgorithmsSupported(), + Matchers.containsInAnyOrder(expectedProofSigningAlgs.toArray())); + }); + try { withCausePropagation(() -> testingClient.server(TEST_REALM_NAME).run((session -> { - KeyAttestationsRequired keyAttestationsRequired = // - Optional.ofNullable(expectedKeyAttestationsRequiredString) - .map(s -> fromJsonString(s, KeyAttestationsRequired.class)) - .orElse(null); - ProofTypesSupported expectedProofTypesSupported = ProofTypesSupported.parse(session, - keyAttestationsRequired, - List.of(Algorithm.RS256)); - assertEquals(expectedProofTypesSupported, - ProofTypesSupported.fromJsonString(proofTypesSupportedString)); + ProofTypesSupported actualProofTypesSupported = ProofTypesSupported.fromJsonString(proofTypesSupportedString); + List actualProofSigningAlgs = actualProofTypesSupported + .getSupportedProofTypes() + .get(ProofType.JWT) + .getSigningAlgorithmsSupported(); + + KeyAttestationsRequired keyAttestationsRequired = // + Optional.ofNullable(expectedKeyAttestationsRequiredString) + .map(s -> fromJsonString(s, KeyAttestationsRequired.class)) + .orElse(null); + + ProofTypesSupported expectedProofTypesSupported = ProofTypesSupported.parse( + session, keyAttestationsRequired, actualProofSigningAlgs); + assertEquals(expectedProofTypesSupported, actualProofTypesSupported); - List expectedSigningAlgs = OID4VCIssuerWellKnownProvider.getSupportedSignatureAlgorithms(session); MatcherAssert.assertThat(signingAlgsSupported, - Matchers.containsInAnyOrder(expectedSigningAlgs.toArray())); + Matchers.containsInAnyOrder(getAllAsymmetricAlgorithms().toArray())); }))); } catch (Throwable e) { throw new RuntimeException(e); @@ -626,6 +646,14 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest compareClaims(expectedFormat, supportedConfig.getCredentialMetadata().getClaims(), clientScope.getProtocolMappers()); } + private static List getAllAsymmetricAlgorithms() { + return List.of( + Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, + Algorithm.RS256, Algorithm.RS384, Algorithm.RS512, + Algorithm.ES256, Algorithm.ES384, Algorithm.ES512, + Algorithm.EdDSA); + } + private void compareDisplay(SupportedCredentialConfiguration supportedConfig, ClientScopeRepresentation clientScope) { String display = clientScope.getAttributes().get(CredentialScopeModel.VC_DISPLAY); if (StringUtil.isBlank(display)) {