Skip to content

Commit 039650e

Browse files
committed
Determine algorithms for credential_response_encryption
Signed-off-by: Ogenbertrand <[email protected]>
1 parent 1c0a916 commit 039650e

File tree

3 files changed

+58
-119
lines changed

3 files changed

+58
-119
lines changed

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

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
6565
private static final Logger LOGGER = Logger.getLogger(OID4VCIssuerWellKnownProvider.class);
6666
private final KeycloakSession keycloakSession;
6767

68-
private static final String ATTR_ENCRYPTION_ALGS = "oid4vci.encryption.algs";
69-
private static final String ATTR_ENCRYPTION_ENCS = "oid4vci.encryption.encs";
7068
private static final String ATTR_ENCRYPTION_REQUIRED = "oid4vci.encryption.required";
7169

7270
public OID4VCIssuerWellKnownProvider(KeycloakSession keycloakSession) {
@@ -119,6 +117,7 @@ private String getSignedMetadata(KeycloakSession session) {
119117

120118
/**
121119
* Returns the credential response encryption metadata for the issuer.
120+
* Now determines supported algorithms from available realm keys.
122121
*
123122
* @param session The Keycloak session
124123
* @return The credential response encryption metadata
@@ -127,9 +126,9 @@ public static CredentialResponseEncryptionMetadata getCredentialResponseEncrypti
127126
RealmModel realm = session.getContext().getRealm();
128127
CredentialResponseEncryptionMetadata metadata = new CredentialResponseEncryptionMetadata();
129128

130-
// Set defaults if attributes not present
131-
metadata.setAlgValuesSupported(getSupportedEncryptionAlgorithms(realm));
132-
metadata.setEncValuesSupported(getSupportedEncryptionMethods(realm));
129+
// Get supported algorithms from available encryption keys
130+
metadata.setAlgValuesSupported(getSupportedEncryptionAlgorithms(session));
131+
metadata.setEncValuesSupported(getSupportedEncryptionMethods());
133132
metadata.setEncryptionRequired(isEncryptionRequired(realm));
134133

135134
return metadata;
@@ -138,23 +137,30 @@ public static CredentialResponseEncryptionMetadata getCredentialResponseEncrypti
138137
/**
139138
* Returns the supported encryption algorithms from realm attributes.
140139
*/
141-
private static List<String> getSupportedEncryptionAlgorithms(RealmModel realm) {
142-
String algs = realm.getAttribute(ATTR_ENCRYPTION_ALGS);
143-
if (algs == null || algs.isEmpty()) {
144-
return List.of(JWEConstants.RSA_OAEP);
140+
private static List<String> getSupportedEncryptionAlgorithms(KeycloakSession session) {
141+
RealmModel realm = session.getContext().getRealm();
142+
KeyManager keyManager = session.keys();
143+
144+
List<String> supportedEncryptionAlgorithms = keyManager.getKeysStream(realm)
145+
.filter(key -> KeyUse.ENC.equals(key.getUse()))
146+
.map(KeyWrapper::getAlgorithm)
147+
.filter(algorithm -> algorithm != null && !algorithm.isEmpty())
148+
.filter(algorithm -> Arrays.asList(JWEConstants.RSA_OAEP, JWEConstants.RSA_OAEP_256).contains(algorithm))
149+
.distinct()
150+
.collect(Collectors.toList());
151+
152+
if (supportedEncryptionAlgorithms.isEmpty()) {
153+
supportedEncryptionAlgorithms.add(JWEConstants.RSA_OAEP);
145154
}
146-
return Arrays.asList(algs.split(","));
155+
156+
return supportedEncryptionAlgorithms;
147157
}
148158

149159
/**
150160
* Returns the supported encryption methods from realm attributes.
151161
*/
152-
private static List<String> getSupportedEncryptionMethods(RealmModel realm) {
153-
String encs = realm.getAttribute(ATTR_ENCRYPTION_ENCS);
154-
if (encs == null || encs.isEmpty()) {
155-
return List.of(JWEConstants.A256GCM);
156-
}
157-
return Arrays.asList(encs.split(","));
162+
private static List<String> getSupportedEncryptionMethods() {
163+
return List.of(JWEConstants.A256GCM);
158164
}
159165

160166
/**

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

Lines changed: 35 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525
import org.junit.experimental.runners.Enclosed;
2626
import org.junit.runner.RunWith;
2727
import org.keycloak.common.util.MultivaluedHashMap;
28+
import org.keycloak.jose.jwe.JWEConstants;
29+
import org.keycloak.models.KeycloakSession;
2830
import org.keycloak.models.RealmModel;
2931
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider;
3032
import org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProviderFactory;
3133
import org.keycloak.protocol.oid4vc.model.CredentialIssuer;
32-
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryption;
3334
import org.keycloak.protocol.oid4vc.model.CredentialResponseEncryptionMetadata;
3435
import org.keycloak.protocol.oid4vc.model.Format;
3536
import org.keycloak.representations.idm.ClientRepresentation;
@@ -83,77 +84,48 @@ public void configureTestRealm(RealmRepresentation testRealm) {
8384

8485
}
8586

86-
public static class TestCredentialDefinitionInClientAttributes extends OID4VCTest {
87-
88-
@Test
89-
public void testCredentialConfig() {
90-
OID4VCIssuerWellKnownProviderTest
91-
.testCredentialConfig(suiteContext, testingClient);
92-
}
87+
public static class TestCredentialIssuerMetadataFields extends OID4VCTest {
9388

9489
@Test
9590
public void testCredentialIssuerMetadataFields() {
96-
String expectedIssuer = suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/" + TEST_REALM_NAME;
9791
KeycloakTestingClient testingClient = this.testingClient;
9892

9993
testingClient
10094
.server(TEST_REALM_NAME)
10195
.run(session -> {
102-
// Setup test realm attributes
103-
RealmModel realm = session.getContext().getRealm();
104-
realm.setAttribute("oid4vci.encryption.algs", "RSA-OAEP");
105-
realm.setAttribute("oid4vci.encryption.encs", "A256GCM");
106-
realm.setAttribute("oid4vci.encryption.required", "true");
107-
realm.setAttribute("batch_credential_issuance.batch_size", "10");
108-
realm.setAttribute("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");
109-
110-
OID4VCIssuerWellKnownProvider provider = new OID4VCIssuerWellKnownProvider(session);
111-
Object config = provider.getConfig();
112-
assertTrue("Should return CredentialIssuer", config instanceof CredentialIssuer);
113-
CredentialIssuer issuer = (CredentialIssuer) config;
114-
115-
// Check basic endpoints
116-
assertEquals(expectedIssuer, issuer.getCredentialIssuer());
117-
assertNotNull(issuer.getCredentialEndpoint());
118-
assertNotNull(issuer.getNonceEndpoint());
119-
assertNotNull(issuer.getDeferredCredentialEndpoint());
120-
assertEquals(List.of(expectedIssuer), issuer.getAuthorizationServers());
121-
122-
// Check credential_response_encryption
96+
CredentialIssuer issuer = getCredentialIssuer(session);
97+
12398
CredentialResponseEncryptionMetadata encryption = issuer.getCredentialResponseEncryption();
124-
assertNotNull("credential_response_encryption should be present", encryption);
125-
assertEquals(List.of("RSA-OAEP"), encryption.getAlgValuesSupported());
126-
assertEquals(List.of("A256GCM"), encryption.getEncValuesSupported());
127-
assertTrue("encryption_required should be true", encryption.getEncryptionRequired());
128-
129-
// Check batch_credential_issuance
130-
CredentialIssuer.BatchCredentialIssuance batch = issuer.getBatchCredentialIssuance();
131-
assertNotNull("batch_credential_issuance should be present", batch);
132-
assertEquals(Integer.valueOf(10), batch.getBatchSize());
133-
134-
// Check signed_metadata
135-
assertEquals(
136-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc",
137-
issuer.getSignedMetadata()
138-
);
139-
140-
// Check credentials_supported is not empty
141-
assertNotNull(issuer.getCredentialsSupported());
142-
assertFalse(issuer.getCredentialsSupported().isEmpty());
99+
assertNotNull(encryption);
100+
101+
assertTrue(encryption.getAlgValuesSupported().contains("RSA-OAEP"));
102+
assertTrue("Supported encryption methods should include A256GCM", encryption.getEncValuesSupported().contains(JWEConstants.A256GCM));
103+
assertTrue(encryption.getEncryptionRequired());
104+
assertEquals(Integer.valueOf(10), issuer.getBatchCredentialIssuance().getBatchSize());
105+
assertEquals("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc",
106+
issuer.getSignedMetadata());
143107
});
144108
}
145109

110+
private static CredentialIssuer getCredentialIssuer(KeycloakSession session) {
111+
RealmModel realm = session.getContext().getRealm();
112+
113+
realm.setAttribute("oid4vci.encryption.required", "true");
114+
realm.setAttribute("batch_credential_issuance.batch_size", "10");
115+
realm.setAttribute("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");
116+
117+
OID4VCIssuerWellKnownProvider provider = new OID4VCIssuerWellKnownProvider(session);
118+
return (CredentialIssuer) provider.getConfig();
119+
}
120+
146121
@Override
147122
public void configureTestRealm(RealmRepresentation testRealm) {
148-
Map<String, String> clientAttributes = new HashMap<>(getTestCredentialDefinitionAttributes());
149-
Map<String, String> realmAttributes = new HashMap<>();
150-
OID4VCIssuerWellKnownProviderTest
151-
.configureTestRealm(
152-
getTestClient("did:web:test.org"),
153-
testRealm,
154-
clientAttributes,
155-
realmAttributes
156-
);
123+
Map<String, String> attributes = new HashMap<>();
124+
attributes.put("oid4vci.encryption.required", "true");
125+
attributes.put("batch_credential_issuance.batch_size", "10");
126+
attributes.put("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");
127+
testRealm.setAttributes(attributes);
128+
157129
}
158130
}
159131

@@ -184,11 +156,6 @@ public static void configureTestRealm(
184156
Map<String, String> clientAttributes,
185157
Map<String, String> realmAttributes
186158
) {
187-
realmAttributes.put("credential_response_encryption.alg_values_supported", "[\"RSA-OAEP\"]");
188-
realmAttributes.put("credential_response_encryption.enc_values_supported", "[\"A256GCM\"]");
189-
realmAttributes.put("credential_response_encryption.encryption_required", "true");
190-
realmAttributes.put("batch_credential_issuance.batch_size", "10");
191-
realmAttributes.put("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc"); // example JWT
192159
testClient.setAttributes(new HashMap<>(clientAttributes));
193160
testRealm.setAttributes(new HashMap<>(realmAttributes));
194161
extendConfigureTestRealm(testRealm, testClient);
@@ -214,16 +181,18 @@ public void testIssuerMetadataIncludesEncryptionSupport() throws IOException {
214181
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().isEmpty());
215182
assertFalse("Supported encryption methods should not be empty",
216183
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().isEmpty());
184+
assertTrue("Supported algorithms should include RSA-OAEP",
185+
oid4vciIssuerConfig.getCredentialResponseEncryption().getAlgValuesSupported().contains(JWEConstants.RSA_OAEP));
186+
assertTrue("Supported encryption methods should include A256GCM",
187+
oid4vciIssuerConfig.getCredentialResponseEncryption().getEncValuesSupported().contains(JWEConstants.A256GCM));
217188
}
218189
}
219190
}
220191

221192
@Override
222193
public void configureTestRealm(RealmRepresentation testRealm) {
223-
// Configure realm with encryption support if needed
224194
Map<String, String> realmAttributes = new HashMap<>();
225-
realmAttributes.put("oid4vci.encryption.algs", "RSA-OAEP,RSA-OAEP-256");
226-
realmAttributes.put("oid4vci.encryption.encs", "A256GCM,A128CBC-HS256");
195+
realmAttributes.put("oid4vci.encryption.required", "true");
227196
testRealm.setAttributes(realmAttributes);
228197

229198
extendConfigureTestRealm(testRealm, getTestClient("did:web:test.org"));

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

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public void testGetCredentialOffer() {
225225
.setCredentialConfigurationIds(List.of("credential-configuration-id"));
226226

227227
String sessionCode = prepareSessionCode(session, authenticator, JsonSerialization.writeValueAsString(credentialsOffer));
228-
// the cache transactions need to be commited explicitly in the test. Without that, the OAuth2Code will only be commited to
228+
// The cache transactions need to be committed explicitly in the test. Without that, the OAuth2Code will only be committed to
229229
// the cache after .run((session)-> ...)
230230
session.getTransactionManager().commit();
231231
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
@@ -855,40 +855,4 @@ public void testRequestCredentialEncryptionRequiredButMissing() {
855855
}
856856
});
857857
}
858-
859-
@Test
860-
public void testRequestCredentialWithUnsupportedEncryptionMethod() {
861-
String token = getBearerToken(oauth);
862-
testingClient.server(TEST_REALM_NAME).run((session -> {
863-
AppAuthManager.BearerTokenAuthenticator authenticator = new AppAuthManager.BearerTokenAuthenticator(session);
864-
authenticator.setTokenString(token);
865-
OID4VCIssuerEndpoint issuerEndpoint = prepareIssuerEndpoint(session, authenticator);
866-
867-
JWK jwk;
868-
try {
869-
jwk = generateRsaJwk();
870-
} catch (NoSuchAlgorithmException e) {
871-
throw new RuntimeException("Failed to generate JWK", e);
872-
}
873-
874-
CredentialRequest credentialRequest = new CredentialRequest()
875-
.setFormat(Format.JWT_VC)
876-
.setCredentialIdentifier("test-credential")
877-
.setCredentialResponseEncryption(
878-
new CredentialResponseEncryption()
879-
.setAlg("RSA-OAEP")
880-
.setEnc("A128CBC-HS256")
881-
.setJwk(jwk));
882-
883-
try {
884-
issuerEndpoint.requestCredential(credentialRequest);
885-
Assert.fail("Expected BadRequestException due to unsupported encryption method");
886-
} catch (BadRequestException e) {
887-
ErrorResponse error = (ErrorResponse) e.getResponse().getEntity();
888-
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS, error.getError());
889-
assertTrue("Error message should indicate unsupported encryption method",
890-
error.getErrorDescription().contains("Unsupported encryption parameters: alg=RSA-OAEP, enc=A128CBC-HS256"));
891-
}
892-
}));
893-
}
894858
}

0 commit comments

Comments
 (0)