Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,18 @@ private CredentialRequest decryptCredentialRequest(String jweString, CredentialR
}
KeyWrapper keyWrapper = matchingKeys.get(0);

String alg = header.getAlgorithm();
if (alg == null) {
throw new JWEException("Missing alg in JWE header");
}
if (!alg.equals(keyWrapper.getAlgorithm())) {
String errorMessage = String.format(
"Key management algorithm mismatch: header alg=%s does not match key algorithm=%s for kid=%s",
alg, keyWrapper.getAlgorithm(), kid);
LOGGER.debug(errorMessage);
throw new JWEException(ErrorType.INVALID_ENCRYPTION_PARAMETERS.getValue());
}

// Set the decryption key
jwe.getKeyStorage().setDecryptionKey(keyWrapper.getPrivateKey());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@

import static org.keycloak.OID4VCConstants.OPENID_CREDENTIAL;
import static org.keycloak.jose.jwe.JWEConstants.A256GCM;
import static org.keycloak.jose.jwe.JWEConstants.RSA1_5;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider.ATTR_REQUEST_ENCRYPTION_REQUIRED;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider.ATTR_RESPONSE_ENCRYPTION_REQUIRED;
import static org.keycloak.tests.oid4vc.OID4VCProofTestUtils.generateJwtProof;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
Expand Down Expand Up @@ -131,6 +133,33 @@ void testEncryptedCredentialRequestWithCompression() throws Exception {
}
}

@Test
void testRequestCredentialRejectsKeyManagementAlgorithmDowngrade() throws Exception {
FlowData flow = prepareFlow();
Map<String, Object> jwkPair = generateRsaJwkWithPrivateKey();
JWK responseJwk = (JWK) jwkPair.get("jwk");

CredentialRequest credentialRequest = new CredentialRequest()
.setCredentialIdentifier(flow.credentialIdentifier())
.setProofs(new Proofs().setJwt(List.of(generateJwtProof(flow.issuer(), flow.cNonce()))))
.setCredentialResponseEncryption(new CredentialResponseEncryption().setEnc(A256GCM).setJwk(responseJwk));

// The issuer advertises this key with an RSA-OAEP family algorithm, never legacy RSA1_5.
// Encrypt the same request to the same RSA public key (same kid), but downgrade the JWE
// header to alg=RSA1_5.
JWK requestEncryptionJwk = flow.issuerMetadata().getCredentialRequestEncryption().getJwks().getKeys()[0];
assertNotEquals(RSA1_5, requestEncryptionJwk.getAlgorithm());

var response = oauth.oid4vc()
.credentialRequest(credentialRequest)
.bearerToken(flow.token())
.encryptRequest(requestEncryptionJwk, false, RSA1_5)
.send();

assertEquals(400, response.getStatusCode());
assertEquals(ErrorType.INVALID_ENCRYPTION_PARAMETERS.getValue(), response.getError());
}

@Test
void testRequestCredentialWithIncompleteEncryptionParams() throws Throwable {
String token = getBearerToken(oauth, client, jwtTypeCredentialScope.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,18 @@ public Oid4vcCredentialRequest payload(String payload, ContentType contentType)
* Encrypt current credential request as compact JWE and send with application/jwt.
*/
public Oid4vcCredentialRequest encryptRequest(JWK issuerEncryptionJwk, boolean useDeflateCompression) {
return encryptRequest(issuerEncryptionJwk, useDeflateCompression, null);
}

/**
* Encrypt current credential request as compact JWE and send with application/jwt, forcing the
* given key management algorithm in the JWE header regardless of the issuer key's advertised
* algorithm. Passing {@code algorithmOverride} exercises algorithm-downgrade scenarios.
*/
public Oid4vcCredentialRequest encryptRequest(JWK issuerEncryptionJwk, boolean useDeflateCompression, String algorithmOverride) {
try {
String requestPayload = JsonSerialization.valueAsString(credRequest);
String jwePayload = encryptPayload(requestPayload, issuerEncryptionJwk, useDeflateCompression);
String jwePayload = encryptPayload(requestPayload, issuerEncryptionJwk, useDeflateCompression, algorithmOverride);
return payload(jwePayload, ContentType.create("application/jwt", StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt credential request", e);
Expand Down Expand Up @@ -102,11 +111,12 @@ protected Oid4vcCredentialResponse toResponse(CloseableHttpResponse response) th
return new Oid4vcCredentialResponse(response);
}

private static String encryptPayload(String payload, JWK issuerEncJwk, boolean useCompression) throws Exception {
private static String encryptPayload(String payload, JWK issuerEncJwk, boolean useCompression, String algorithmOverride) throws Exception {
PublicKey publicKey = JWKParser.create(issuerEncJwk).toPublicKey();
String algorithm = algorithmOverride != null ? algorithmOverride : issuerEncJwk.getAlgorithm();
JWEHeader.JWEHeaderBuilder builder = new JWEHeader.JWEHeaderBuilder()
.keyId(issuerEncJwk.getKeyId())
.algorithm(issuerEncJwk.getAlgorithm())
.algorithm(algorithm)
.encryptionAlgorithm("A256GCM")
.type("JWT");
if (useCompression) {
Expand Down
Loading