diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java b/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java index fb1cfe3b532..99a78dbb04d 100644 --- a/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java +++ b/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java @@ -209,19 +209,32 @@ public class SdJwtVP { return issuerSignedJWT.getCnfClaim().orElse(null); } + /** + * Create new Sd-JWT presentation from this Sd-JWT + * + * @param disclosureDigests Disclosure digests (hashes) of the claims to disclose. + * @param discloseAllClaims When the parameter is true, then disclosureDigests parameter is ignored and everything is presented. When false, then only claims specified + * by disclosureDigests are presented + * @param keyBindingClaims Key binding claims. When omitted, created presentation may not contain key-binding + * @param holdSignatureSignerContext Useful for signing the key-binding JWT + * @return String with new Sd-JWT presentation with added key-binding and selected disclosed claims + */ public String present(List disclosureDigests, + boolean discloseAllClaims, ObjectNode keyBindingClaims, SignatureSignerContext holdSignatureSignerContext) { StringBuilder sb = new StringBuilder(); - if (disclosureDigests == null || disclosureDigests.isEmpty()) { + if (discloseAllClaims) { // disclose everything sb.append(sdJwtVpString); } else { sb.append(issuerSignedJWT.getJws()); sb.append(SDJWT_DELIMITER); - for (String disclosureDigest : disclosureDigests) { - sb.append(disclosures.get(disclosureDigest)); - sb.append(SDJWT_DELIMITER); + if (disclosureDigests != null) { + for (String disclosureDigest : disclosureDigests) { + sb.append(disclosures.get(disclosureDigest)); + sb.append(SDJWT_DELIMITER); + } } } String unboundPresentation = sb.toString(); @@ -238,6 +251,42 @@ public class SdJwtVP { return sb.toString(); } + + /** + * Create new Sd-JWT presentation from this Sd-JWT. It works same like {@link #present(List, boolean, ObjectNode, SignatureSignerContext)} but it allows + * to specify the names of the claims to present (EG. given_name, family_name) instead of specifying disclosureDigests + * + * @param claimsToDisclose Names of the claims to disclose (EG. given_name, family_name) + * @param discloseAllClaims Used in case that claimsToDisclose is empty or null. In case this is true, all the claims from this SdJWT will be disclosed. + * If it is false, then only claims specified by claimsToDisclose parameter would be disclosed + * @param keyBindingClaims Key binding claims. When omitted, created presentation may not contain key-binding + * @param holdSignatureSignerContext Useful for signing the key-binding JWT + * @return String with new Sd-JWT presentation with added key-binding and selected disclosed claims + */ + public String presentWithSpecifiedClaims(List claimsToDisclose, + boolean discloseAllClaims, + ObjectNode keyBindingClaims, + SignatureSignerContext holdSignatureSignerContext) { + if (discloseAllClaims) { + return present(null, true, keyBindingClaims, holdSignatureSignerContext); + } else { + List digests = getClaims().entrySet().stream() + .filter(entry -> { + ArrayNode node = entry.getValue(); + if (node.size() >= 2) { + String claimName = node.get(1).asText(); + return (claimsToDisclose.contains(claimName)); + } + return false; + }) + .map(Map.Entry::getKey) + .sorted() + .collect(Collectors.toList()); + + return present(digests, false, keyBindingClaims, holdSignatureSignerContext); + } + } + /** * Verifies SD-JWT presentation. * diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java index fa2a597e3b8..0a391b91987 100644 --- a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java @@ -17,6 +17,11 @@ package org.keycloak.sdjwt.sdjwtvp; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.keycloak.common.VerificationException; import org.keycloak.crypto.Algorithm; @@ -30,10 +35,12 @@ import org.keycloak.sdjwt.vp.SdJwtVP; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -150,7 +157,7 @@ public abstract class SdJwtVPTest { String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); ObjectNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); - String presentation = sdJwtVP.present(null, keyBindingClaims, + String presentation = sdJwtVP.present(null, true, keyBindingClaims, TestSettings.getInstance().getHolderSignerContext()); SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); @@ -162,6 +169,8 @@ public abstract class SdJwtVPTest { // Verify with public key from cnf claim presenteSdJwtVP.getKeyBindingJWT().get() .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), Algorithm.ES256)); + + assertExpectedClaims(presenteSdJwtVP, Arrays.asList("address", "given_name", "family_name")); } @Test(expected = VerificationException.class) @@ -169,7 +178,7 @@ public abstract class SdJwtVPTest { String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); ObjectNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); - String presentation = sdJwtVP.present(null, keyBindingClaims, + String presentation = sdJwtVP.present(null, true, keyBindingClaims, TestSettings.getInstance().getHolderSignerContext()); SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); @@ -188,7 +197,7 @@ public abstract class SdJwtVPTest { SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); ObjectNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); // disclose only the given_name - String presentation = sdJwtVP.present(Arrays.asList("jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"), + String presentation = sdJwtVP.present(Arrays.asList("jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"), false, keyBindingClaims, TestSettings.getInstance().getHolderSignerContext()); SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); @@ -197,6 +206,79 @@ public abstract class SdJwtVPTest { // Verify with public key from cnf claim presenteSdJwtVP.getKeyBindingJWT().get() .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), Algorithm.ES256)); + + assertExpectedClaims(presenteSdJwtVP, Collections.singletonList("given_name")); + } + + @Test + public void testPresentationWithoutDisclosures() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + ObjectNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + + // Presentation without any disclosed claims + String presentation = sdJwtVP.present(Collections.emptyList(), false, + keyBindingClaims, TestSettings.getInstance().getHolderSignerContext()); + + SdJwtVP presentedSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presentedSdJwtVP.getKeyBindingJWT().isPresent()); + + // Verify with public key from cnf claim + presentedSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presentedSdJwtVP.getCnfClaim(), Algorithm.ES256)); + + // Assert no claims disclosed + assertExpectedClaims(presentedSdJwtVP, Collections.emptyList()); + } + + @Test + public void testPresentationOfSpecifiedClaims() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + ObjectNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + + // Disclosures of family_name and given_name + String presentation = sdJwtVP.present(Arrays.asList("TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo", "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"), + false, null, null); + + // Creating presentation with directly specifying claims I want to disclose + String presentation2 = sdJwtVP.presentWithSpecifiedClaims(Arrays.asList("given_name", "family_name"), false, + null, null); + + Assert.assertEquals(presentation, presentation2); + + // Specifying not-existent claims works as well. Non-existent claim is ignored + String presentation3 = sdJwtVP.presentWithSpecifiedClaims(Arrays.asList("given_name", "family_name", "non-existent"), false, + null, null); + Assert.assertEquals(presentation, presentation3); + + // Test with key-binding not present + SdJwtVP presentedSdJwtVP = SdJwtVP.of(presentation); + assertFalse(presentedSdJwtVP.getKeyBindingJWT().isPresent()); + + // Test with key-binding present + String presentation4 = sdJwtVP.presentWithSpecifiedClaims(Arrays.asList("given_name", "family_name"), false, + keyBindingClaims, TestSettings.getInstance().getHolderSignerContext()); + + presentedSdJwtVP = SdJwtVP.of(presentation4); + assertTrue(presentedSdJwtVP.getKeyBindingJWT().isPresent()); + + // Verify with public key from cnf claim + presentedSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presentedSdJwtVP.getCnfClaim(), Algorithm.ES256)); + + // Assert only given_name and family_name claims disclosed in the new presentation + assertExpectedClaims(presentedSdJwtVP, Arrays.asList("given_name", "family_name")); + } + + private void assertExpectedClaims(SdJwtVP presentedSdJwtVP, List expectedClaims) { + Set availableClaims = presentedSdJwtVP.getClaims().values() + .stream() + .filter(arrayNode -> arrayNode.size() == 3) // Filter array claims + .map(arrayNode -> arrayNode.get(1).asText()) + .collect(Collectors.toSet()); + + Assert.assertEquals(availableClaims, new HashSet<>(expectedClaims)); }