mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
More capabilities in SdJwtVP API when creating presentations (#44977)
closes #44976 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
4b68f6998b
commit
92314bccc6
@ -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<String> 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<String> claimsToDisclose,
|
||||
boolean discloseAllClaims,
|
||||
ObjectNode keyBindingClaims,
|
||||
SignatureSignerContext holdSignatureSignerContext) {
|
||||
if (discloseAllClaims) {
|
||||
return present(null, true, keyBindingClaims, holdSignatureSignerContext);
|
||||
} else {
|
||||
List<String> 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.
|
||||
*
|
||||
|
||||
@ -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<String> expectedClaims) {
|
||||
Set<String> 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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user