Skip to content

Commit 94ee6d8

Browse files
Vitalisn4mposoldaIngridPuppet
authored
[OID4VCI] Realign naming of attribute configuring algorithms for credential (keycloak#44765)
Closes keycloak#44621 Signed-off-by: Vitalisn4 <[email protected]> Signed-off-by: mposolda <[email protected]> Signed-off-by: Ingrid Kamga <[email protected]> Co-authored-by: Marek Posolda <[email protected]> Co-authored-by: Ingrid Kamga <[email protected]>
1 parent 5ae60f3 commit 94ee6d8

File tree

5 files changed

+60
-53
lines changed

5 files changed

+60
-53
lines changed

docs/documentation/server_admin/topics/oid4vci/vc-issuer-configuration.adoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ Create a JSON file (e.g., `client-scopes.json`) with the following content:
248248
"vc.verifiable_credential_type": "my-vct",
249249
"vc.supported_credential_types": "credential-type-1,credential-type-2",
250250
"vc.credential_contexts": "context-1,context-2",
251-
"vc.proof_signing_alg_values_supported": "ES256",
251+
"vc.credential_signing_alg": "ES256",
252252
"vc.cryptographic_binding_methods_supported": "jwk",
253253
"vc.signing_key_id": "key-id-123456",
254254
"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}+`
358358
| The context values of the Verifiable Credential Type. +
359359
_Default_: `$\{name}+`
360360

361-
| `vc.proof_signing_alg_values_supported`
361+
| `vc.credential_signing_alg`
362362
| optional
363-
| Supported signature algorithms for this credential. +
364-
_Default_: All present keys supporting JWS algorithms in the realm.
363+
| Supported signature algorithm for this credential. +
364+
_Default_: All asymmetric signing algorithms backed by realm keys.
365365

366366
| `vc.cryptographic_binding_methods_supported`
367367
| optional

server-spi-private/src/main/java/org/keycloak/models/oid4vci/CredentialScopeModel.java

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,9 @@ public class CredentialScopeModel implements ClientScopeModel {
7272
public static final String CONTEXTS = "vc.credential_contexts";
7373

7474
/**
75-
* if the credential is only meant for specific signing algorithms the global default list can be overridden here.
76-
* The global default list is retrieved from the available keys in the realm.
75+
* The credential signature algorithm. If it is not configured, then the realm active key is used to sign the verifiable credential
7776
*/
78-
public static final String SIGNING_ALG_VALUES_SUPPORTED = "vc.proof_signing_alg_values_supported";
77+
public static final String SIGNING_ALG = "vc.credential_signing_alg";
7978

8079
/**
8180
* if the credential is only meant for specific cryptographic binding algorithms the global default list can be
@@ -269,20 +268,12 @@ public void setVcContexts(List<String> vcContexts) {
269268
clientScope.setAttribute(CONTEXTS, String.join(",", vcContexts));
270269
}
271270

272-
public List<String> getSigningAlgsSupported() {
273-
return Optional.ofNullable(clientScope.getAttribute(SIGNING_ALG_VALUES_SUPPORTED))
274-
.map(s -> s.split(","))
275-
.map(Arrays::asList)
276-
.orElse(Collections.emptyList());
277-
}
278-
279-
public void setSigningAlgsSupported(String signingAlgsSupported) {
280-
clientScope.setAttribute(SIGNING_ALG_VALUES_SUPPORTED, signingAlgsSupported);
271+
public String getSigningAlg() {
272+
return clientScope.getAttribute(SIGNING_ALG);
281273
}
282274

283-
public void setSigningAlgsSupported(List<String> signingAlgsSupported) {
284-
clientScope.setAttribute(SIGNING_ALG_VALUES_SUPPORTED,
285-
String.join(",", signingAlgsSupported));
275+
public void setSigningAlg(String signingAlg) {
276+
clientScope.setAttribute(SIGNING_ALG, signingAlg);
286277
}
287278

288279
public List<String> getCryptographicBindingMethods() {

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerWellKnownProvider.java

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ private static boolean isEncryptionRequired(RealmModel realm) {
446446
* and the credentials supported by the clients available in the session.
447447
*/
448448
public static Map<String, SupportedCredentialConfiguration> getSupportedCredentials(KeycloakSession keycloakSession) {
449-
List<String> globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession);
449+
List<String> globalSupportedSigningAlgorithms = getSupportedAsymmetricSignatureAlgorithms(keycloakSession);
450450

451451
RealmModel realm = keycloakSession.getContext().getRealm();
452452
Map<String, SupportedCredentialConfiguration> supportedCredentialConfigurations =
@@ -466,7 +466,8 @@ public static Map<String, SupportedCredentialConfiguration> getSupportedCredenti
466466

467467
public static SupportedCredentialConfiguration toSupportedCredentialConfiguration(KeycloakSession keycloakSession,
468468
CredentialScopeModel credentialModel) {
469-
List<String> globalSupportedSigningAlgorithms = getSupportedSignatureAlgorithms(keycloakSession);
469+
List<String> globalSupportedSigningAlgorithms = getSupportedAsymmetricSignatureAlgorithms(keycloakSession);
470+
470471
return SupportedCredentialConfiguration.parse(keycloakSession,
471472
credentialModel,
472473
globalSupportedSigningAlgorithms);
@@ -496,18 +497,6 @@ public static String getCredentialsEndpoint(KeycloakContext context) {
496497
return getIssuer(context) + "/protocol/" + OID4VCLoginProtocolFactory.PROTOCOL_ID + "/" + OID4VCIssuerEndpoint.CREDENTIAL_PATH;
497498
}
498499

499-
public static List<String> getSupportedSignatureAlgorithms(KeycloakSession session) {
500-
RealmModel realm = session.getContext().getRealm();
501-
KeyManager keyManager = session.keys();
502-
503-
return keyManager.getKeysStream(realm)
504-
.filter(key -> KeyUse.SIG.equals(key.getUse()))
505-
.map(KeyWrapper::getAlgorithm)
506-
.filter(algorithm -> algorithm != null && !algorithm.isEmpty())
507-
.distinct()
508-
.collect(Collectors.toList());
509-
}
510-
511500
/**
512501
* Return the authorization servers from the issuer configuration.
513502
*/

services/src/main/java/org/keycloak/protocol/oid4vc/model/SupportedCredentialConfiguration.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323

2424
import org.keycloak.models.KeycloakSession;
2525
import org.keycloak.models.oid4vci.CredentialScopeModel;
26+
import org.keycloak.utils.StringUtil;
2627

2728
import com.fasterxml.jackson.annotation.JsonIgnore;
2829
import com.fasterxml.jackson.annotation.JsonInclude;
2930
import com.fasterxml.jackson.annotation.JsonProperty;
30-
import org.apache.commons.collections4.ListUtils;
3131

3232
/**
3333
* A supported credential, as used in the Credentials Issuer Metadata in OID4VCI
@@ -118,13 +118,12 @@ public static SupportedCredentialConfiguration parse(KeycloakSession keycloakSes
118118
ProofTypesSupported proofTypesSupported = ProofTypesSupported.parse(keycloakSession,
119119
keyAttestationsRequired,
120120
globalSupportedSigningAlgorithms);
121-
credentialConfiguration.setProofTypesSupported(proofTypesSupported);
121+
credentialConfiguration.setProofTypesSupported(proofTypesSupported);
122122

123-
List<String> signingAlgsSupported = credentialScope.getSigningAlgsSupported();
124-
signingAlgsSupported = signingAlgsSupported.isEmpty() ? globalSupportedSigningAlgorithms :
125-
// if the config has listed different algorithms than supported by keycloak we must use the
126-
// intersection of the configuration with the actual supported algorithms.
127-
ListUtils.intersection(signingAlgsSupported, globalSupportedSigningAlgorithms);
123+
// Return single configured value for the signature algorithm if any
124+
String signingAlgSupported = credentialScope.getSigningAlg();
125+
List<String> signingAlgsSupported = StringUtil.isBlank(signingAlgSupported) ? globalSupportedSigningAlgorithms :
126+
Collections.singletonList(signingAlgSupported);
128127
credentialConfiguration.setCredentialSigningAlgValuesSupported(signingAlgsSupported);
129128

130129
// TODO resolve value dynamically from provider implementations?

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerWellKnownProviderTest.java

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.keycloak.protocol.oid4vc.model.Format;
6262
import org.keycloak.protocol.oid4vc.model.JWTVCIssuerMetadata;
6363
import org.keycloak.protocol.oid4vc.model.KeyAttestationsRequired;
64+
import org.keycloak.protocol.oid4vc.model.ProofType;
6465
import org.keycloak.protocol.oid4vc.model.ProofTypesSupported;
6566
import org.keycloak.protocol.oid4vc.model.SupportedCredentialConfiguration;
6667
import org.keycloak.representations.idm.ClientScopeRepresentation;
@@ -585,9 +586,15 @@ private void compareMetadataToClientScope(CredentialIssuer credentialIssuer, Cli
585586
Matchers.containsInAnyOrder(credentialDefinitionTypes.toArray()));
586587

587588
List<String> signingAlgsSupported = new ArrayList<>(supportedConfig.getCredentialSigningAlgValuesSupported());
588-
String proofTypesSupportedString = supportedConfig.getProofTypesSupported().toJsonString();
589+
ProofTypesSupported proofTypesSupported = supportedConfig.getProofTypesSupported();
590+
String proofTypesSupportedString = proofTypesSupported.toJsonString();
589591

590-
KeyAttestationsRequired expectedKeyAttestationsRequired = null;
592+
MatcherAssert.assertThat(proofTypesSupported.getSupportedProofTypes().keySet(),
593+
Matchers.containsInAnyOrder(ProofType.JWT, ProofType.ATTESTATION));
594+
595+
List<String> expectedProofSigningAlgs = getAllAsymmetricAlgorithms();
596+
597+
KeyAttestationsRequired expectedKeyAttestationsRequired;
591598
if (Boolean.parseBoolean(clientScope.getAttributes().get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED))) {
592599
expectedKeyAttestationsRequired = new KeyAttestationsRequired();
593600
expectedKeyAttestationsRequired.setKeyStorage(
@@ -600,24 +607,37 @@ private void compareMetadataToClientScope(CredentialIssuer credentialIssuer, Cli
600607
.get(CredentialScopeModel.KEY_ATTESTATION_REQUIRED_USER_AUTH))
601608
.map(s -> Arrays.asList(s.split(",")))
602609
.orElse(null));
610+
} else {
611+
expectedKeyAttestationsRequired = null;
603612
}
604613
String expectedKeyAttestationsRequiredString = toJsonString(expectedKeyAttestationsRequired);
605614

615+
proofTypesSupported.getSupportedProofTypes().values()
616+
.forEach(proofTypeData -> {
617+
assertEquals(expectedKeyAttestationsRequired, proofTypeData.getKeyAttestationsRequired());
618+
MatcherAssert.assertThat(proofTypeData.getSigningAlgorithmsSupported(),
619+
Matchers.containsInAnyOrder(expectedProofSigningAlgs.toArray()));
620+
});
621+
606622
try {
607623
withCausePropagation(() -> testingClient.server(TEST_REALM_NAME).run((session -> {
624+
ProofTypesSupported actualProofTypesSupported = ProofTypesSupported.fromJsonString(proofTypesSupportedString);
625+
List<String> actualProofSigningAlgs = actualProofTypesSupported
626+
.getSupportedProofTypes()
627+
.get(ProofType.JWT)
628+
.getSigningAlgorithmsSupported();
629+
608630
KeyAttestationsRequired keyAttestationsRequired = //
609-
Optional.ofNullable(expectedKeyAttestationsRequiredString)
610-
.map(s -> fromJsonString(s, KeyAttestationsRequired.class))
611-
.orElse(null);
612-
ProofTypesSupported expectedProofTypesSupported = ProofTypesSupported.parse(session,
613-
keyAttestationsRequired,
614-
List.of(Algorithm.RS256));
615-
assertEquals(expectedProofTypesSupported,
616-
ProofTypesSupported.fromJsonString(proofTypesSupportedString));
617-
618-
List<String> expectedSigningAlgs = OID4VCIssuerWellKnownProvider.getSupportedSignatureAlgorithms(session);
631+
Optional.ofNullable(expectedKeyAttestationsRequiredString)
632+
.map(s -> fromJsonString(s, KeyAttestationsRequired.class))
633+
.orElse(null);
634+
635+
ProofTypesSupported expectedProofTypesSupported = ProofTypesSupported.parse(
636+
session, keyAttestationsRequired, actualProofSigningAlgs);
637+
assertEquals(expectedProofTypesSupported, actualProofTypesSupported);
638+
619639
MatcherAssert.assertThat(signingAlgsSupported,
620-
Matchers.containsInAnyOrder(expectedSigningAlgs.toArray()));
640+
Matchers.containsInAnyOrder(getAllAsymmetricAlgorithms().toArray()));
621641
})));
622642
} catch (Throwable e) {
623643
throw new RuntimeException(e);
@@ -626,6 +646,14 @@ private void compareMetadataToClientScope(CredentialIssuer credentialIssuer, Cli
626646
compareClaims(expectedFormat, supportedConfig.getCredentialMetadata().getClaims(), clientScope.getProtocolMappers());
627647
}
628648

649+
private static List<String> getAllAsymmetricAlgorithms() {
650+
return List.of(
651+
Algorithm.PS256, Algorithm.PS384, Algorithm.PS512,
652+
Algorithm.RS256, Algorithm.RS384, Algorithm.RS512,
653+
Algorithm.ES256, Algorithm.ES384, Algorithm.ES512,
654+
Algorithm.EdDSA);
655+
}
656+
629657
private void compareDisplay(SupportedCredentialConfiguration supportedConfig, ClientScopeRepresentation clientScope) {
630658
String display = clientScope.getAttributes().get(CredentialScopeModel.VC_DISPLAY);
631659
if (StringUtil.isBlank(display)) {

0 commit comments

Comments
 (0)