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
9 changes: 9 additions & 0 deletions core/src/main/java/org/keycloak/sdjwt/JwsToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ public String sign(SignatureSignerContext signerContext) {

public void verifySignature(SignatureVerifierContext verifier) throws VerificationException {
Objects.requireNonNull(verifier, "verifier must not be null");
String headerAlgorithm = jwsHeader == null || jwsHeader.getAlgorithm() == null
? null
: jwsHeader.getRawAlgorithm();
String verifierAlgorithm = verifier.getAlgorithm();
if (headerAlgorithm == null || verifierAlgorithm == null || !headerAlgorithm.equals(verifierAlgorithm)) {
throw new VerificationException(String.format(
"JWS header algorithm '%s' does not match verifier algorithm '%s'",
headerAlgorithm, verifierAlgorithm));
}
try {
if (!verifier.verify(jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8),
jwsInput.getSignature())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.keycloak.OID4VCConstants;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.jose.jws.JWSHeader;
Expand Down Expand Up @@ -81,6 +82,47 @@ public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
}
}

@Test
public void sdJwtVerificationShouldFail_WhenHeaderAlgorithmDiffersFromVerifierAlgorithm() {
SdJwt sdJwt = SdJwt.builder()
.withIssuerSignedJwt(exampleFlatSdJwtV1().build())
.withIssuerSigningContext(TestSettings.signerWithReportedAlgorithm(
testSettings.issuerSigContext, Algorithm.ES384))
.build();

assertEquals(Algorithm.ES384, sdJwt.getIssuerSignedJWT().getJwsHeader().getRawAlgorithm());
assertEquals(Algorithm.ES256, testSettings.issuerVerifierContext.getAlgorithm());

VerificationException exception = assertThrows(
VerificationException.class,
() -> sdJwt.verify(
defaultIssuerVerifyingKeys(),
optionalTimeClaimVerificationOpts().build())
);

assertEquals("Invalid Issuer-Signed JWT: Signature could not be verified", exception.getMessage());
}

@Test
public void sdJwtVerificationShouldFail_WhenHeaderAlgorithmIsMissing() {
SdJwt signedSdJwt = SdJwt.builder()
.withIssuerSignedJwt(exampleFlatSdJwtV1().build())
.withIssuerSigningContext(testSettings.issuerSigContext)
.build();
String jwsWithoutAlgorithm = TestUtils.removeAlgorithmFromJwsHeader(
signedSdJwt.getIssuerSignedJWT().getJws());
SdJwt sdJwt = new SdJwt(new IssuerSignedJWT(jwsWithoutAlgorithm), null);

VerificationException exception = assertThrows(
VerificationException.class,
() -> sdJwt.verify(
defaultIssuerVerifyingKeys(),
optionalTimeClaimVerificationOpts().build())
);

assertEquals("Invalid Issuer-Signed JWT: Signature could not be verified", exception.getMessage());
}

@Test
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
IssuerSignedJWT issuerSignedJWT = exampleFlatSdJwtV1().build();
Expand Down
26 changes: 26 additions & 0 deletions core/src/test/java/org/keycloak/sdjwt/TestSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.keycloak.crypto.ECDSASignatureVerifierContext;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;

Expand Down Expand Up @@ -79,6 +80,31 @@ public SignatureVerifierContext getHolderVerifierContext() {
return holderVerifierContext;
}

public static SignatureSignerContext signerWithReportedAlgorithm(SignatureSignerContext delegate,
String reportedAlgorithm) {
return new SignatureSignerContext() {
@Override
public String getKid() {
return delegate.getKid();
}

@Override
public String getAlgorithm() {
return reportedAlgorithm;
}

@Override
public String getHashAlgorithm() {
return delegate.getHashAlgorithm();
}

@Override
public byte[] sign(byte[] data) throws SignatureException {
return delegate.sign(data);
}
};
}

// private constructor
private TestSettings() {
JsonNode testSettings = TestUtils.readClaimSet(getClass(), "sdjwt/test-settings.json");
Expand Down
18 changes: 18 additions & 0 deletions core/src/test/java/org/keycloak/sdjwt/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import java.io.InputStreamReader;
import java.util.Objects;

import org.keycloak.common.util.Base64Url;

import com.fasterxml.jackson.databind.node.ObjectNode;

/**
Expand Down Expand Up @@ -61,4 +63,20 @@ public static String splitStringIntoLines(String input, int lineLength) {
return result.toString();
}

public static String removeAlgorithmFromJwsHeader(String jws) {
String[] parts = jws.split("\\.", -1);
if (parts.length != 3) {
throw new IllegalArgumentException("Expected a compact JWS with three parts");
}

try {
ObjectNode header = (ObjectNode) SdJwtUtils.mapper.readTree(Base64Url.decode(parts[0]));
header.remove("alg");
parts[0] = Base64Url.encode(SdJwtUtils.mapper.writeValueAsBytes(header));
return String.join(".", parts);
} catch (IOException e) {
throw new IllegalArgumentException("Could not rewrite JWS header", e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.keycloak.OID4VCConstants;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.rule.CryptoInitRule;
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
Expand Down Expand Up @@ -187,6 +189,53 @@ public void testShouldFail_IfKeyBindingJwtSignatureInvalid() {
);
}

@Test
public void testShouldFail_IfKeyBindingHeaderAlgorithmDiffersFromVerifierAlgorithm() {
SignatureSignerContext signer = TestSettings.signerWithReportedAlgorithm(
testSettings.holderSigContext, Algorithm.ES384);
SdJwtVP sdJwtVP = exampleSdJwtWithCustomKbPayload(exampleKbPayload(), signer);

assertEquals(Algorithm.ES384,
sdJwtVP.getKeyBindingJWT().orElseThrow(AssertionError::new).getJwsHeader().getRawAlgorithm());

VerificationException exception = assertThrows(
VerificationException.class,
() -> sdJwtVP.verify(
defaultIssuerVerifyingKeys(),
defaultIssuerSignedJwtVerificationOpts().build(),
defaultKeyBindingJwtVerificationOpts().build())
);

assertEquals("Key binding JWT invalid", exception.getMessage());
assertEquals("JWS header algorithm 'ES384' does not match verifier algorithm 'ES256'",
exception.getCause().getMessage());
}

@Test
public void testShouldFail_IfKeyBindingHeaderAlgorithmIsMissing() {
KeyBindingJWT keyBindingJWT = KeyBindingJWT.builder()
.withPayload(exampleKbPayload())
.withSignerContext(testSettings.holderSigContext)
.build();
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
String sdJwtWithoutKb = sdJwtVPString.substring(
0, sdJwtVPString.lastIndexOf(OID4VCConstants.SDJWT_DELIMITER) + 1);
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtWithoutKb
+ TestUtils.removeAlgorithmFromJwsHeader(keyBindingJWT.getJws()));

VerificationException exception = assertThrows(
VerificationException.class,
() -> sdJwtVP.verify(
defaultIssuerVerifyingKeys(),
defaultIssuerSignedJwtVerificationOpts().build(),
defaultKeyBindingJwtVerificationOpts().build())
);

assertEquals("Key binding JWT invalid", exception.getMessage());
assertEquals("JWS header algorithm 'null' does not match verifier algorithm 'ES256'",
exception.getCause().getMessage());
}

@Test
public void testShouldFail_IfNoCnfClaim() {
testShouldFailGeneric(
Expand Down Expand Up @@ -512,9 +561,14 @@ private ObjectNode exampleKbPayload() {
}

private SdJwtVP exampleSdJwtWithCustomKbPayload(ObjectNode kbPayloadSubstitute) {
return exampleSdJwtWithCustomKbPayload(kbPayloadSubstitute, testSettings.holderSigContext);
}

private SdJwtVP exampleSdJwtWithCustomKbPayload(ObjectNode kbPayloadSubstitute,
SignatureSignerContext signerContext) {
KeyBindingJWT keyBindingJWT = KeyBindingJWT.builder()
.withPayload(kbPayloadSubstitute)
.withSignerContext(testSettings.holderSigContext)
.withSignerContext(signerContext)
.build();

String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
Expand Down
Loading