[OID4VCI] Realign naming of attribute configuring algorithms for credential (#44765)

Closes #44621


Signed-off-by: Vitalisn4 <ngamvitalisyuh@gmail.com>
Signed-off-by: mposolda <mposolda@gmail.com>
Signed-off-by: Ingrid Kamga <Ingrid.Kamga@adorsys.com>
Co-authored-by: Marek Posolda <mposolda@gmail.com>
Co-authored-by: Ingrid Kamga <Ingrid.Kamga@adorsys.com>
This commit is contained in:
Palpable
2025-12-16 14:46:17 +01:00
committed by GitHub
parent 5ae60f3513
commit 94ee6d81fb
5 changed files with 60 additions and 53 deletions

View File

@@ -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

View File

@@ -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<String> 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<String> signingAlgsSupported) {
clientScope.setAttribute(SIGNING_ALG_VALUES_SUPPORTED,
String.join(",", signingAlgsSupported));
public void setSigningAlg(String signingAlg) {
clientScope.setAttribute(SIGNING_ALG, signingAlg);
}
public List<String> getCryptographicBindingMethods() {

View File

@@ -446,7 +446,7 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
* and the credentials supported by the clients available in the session.
*/
public static Map<String, SupportedCredentialConfiguration> getSupportedCredentials(KeycloakSession keycloakSession) {
List<String> globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession);
List<String> globalSupportedSigningAlgorithms = getSupportedAsymmetricSignatureAlgorithms(keycloakSession);
RealmModel realm = keycloakSession.getContext().getRealm();
Map<String, SupportedCredentialConfiguration> supportedCredentialConfigurations =
@@ -466,7 +466,8 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
public static SupportedCredentialConfiguration toSupportedCredentialConfiguration(KeycloakSession keycloakSession,
CredentialScopeModel credentialModel) {
List<String> globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession);
List<String> 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<String> 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.
*/

View File

@@ -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<String> 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<String> signingAlgsSupported = StringUtil.isBlank(signingAlgSupported) ? globalSupportedSigningAlgorithms :
Collections.singletonList(signingAlgSupported);
credentialConfiguration.setCredentialSigningAlgValuesSupported(signingAlgsSupported);
// TODO resolve value dynamically from provider implementations?

View File

@@ -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<String> 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<String> 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<String> 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<String> 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<String> 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)) {