OID4VC: Keycloak native support of SD-JWT (#25829)

Closes #25638


Signed-off-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
Francis Pouatcha 2024-02-19 17:56:18 +01:00 committed by GitHub
parent aa6b102e3d
commit f7e60b4338
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 3981 additions and 0 deletions

1
.gitignore vendored
View file

@ -65,6 +65,7 @@ nbproject
# Maven # # Maven #
######### #########
target target
bin
# Maven shade # Maven shade
############# #############

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public abstract class AbstractSdJwtClaim implements SdJwtClaim {
private final SdJwtClaimName claimName;
public AbstractSdJwtClaim(SdJwtClaimName claimName) {
this.claimName = claimName;
}
@Override
public SdJwtClaimName getClaimName() {
return claimName;
}
@Override
public String getClaimNameAsString() {
return claimName.toString();
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
/**
* Handles selective disclosure of elements within a top-level array claim,
* supporting both visible and undisclosed elements.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class ArrayDisclosure extends AbstractSdJwtClaim {
private final List<SdJwtArrayElement> elements;
private JsonNode visibleClaimValue = null;
private final List<DecoyArrayElement> decoyElements;
private ArrayDisclosure(SdJwtClaimName claimName, List<SdJwtArrayElement> elements,
List<DecoyArrayElement> decoyElements) {
super(claimName);
this.elements = elements;
this.decoyElements = decoyElements;
}
/**
* Print the array with visible and invisible elements.
*/
@Override
public JsonNode getVisibleClaimValue(String hashAlgo) {
if (visibleClaimValue != null)
return visibleClaimValue;
List<JsonNode> visibleElts = new ArrayList<>();
elements.stream()
.filter(Objects::nonNull)
.forEach(e -> visibleElts.add(e.getVisibleValue(hashAlgo)));
decoyElements.stream()
.filter(Objects::nonNull)
.forEach(e -> {
if (e.getIndex() < visibleElts.size())
visibleElts.add(e.getIndex(), e.getVisibleValue(hashAlgo));
else
visibleElts.add(e.getVisibleValue(hashAlgo));
});
final ArrayNode n = SdJwtUtils.mapper.createArrayNode();
visibleElts.forEach(n::add);
visibleClaimValue = n;
return visibleClaimValue;
}
@Override
public List<String> getDisclosureStrings() {
final List<String> disclosureStrings = new ArrayList<>();
elements.stream()
.filter(Objects::nonNull)
.forEach(e -> {
String disclosureString = e.getDisclosureString();
if (disclosureString != null)
disclosureStrings.add(disclosureString);
});
return disclosureStrings;
}
public static class Builder {
private SdJwtClaimName claimName;
private final List<SdJwtArrayElement> elements = new ArrayList<>();
private final List<DecoyArrayElement> decoyElements = new ArrayList<>();
public Builder withClaimName(String claimName) {
this.claimName = new SdJwtClaimName(claimName);
return this;
}
public Builder withVisibleElement(JsonNode elementValue) {
this.elements.add(new VisibleArrayElement(elementValue));
return this;
}
public Builder withUndisclosedElement(SdJwtSalt salt, JsonNode elementValue) {
SdJwtSalt sdJwtSalt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt;
this.elements.add(UndisclosedArrayElement.builder()
.withSalt(sdJwtSalt)
.withArrayElement(elementValue)
.build());
return this;
}
public void withDecoyElt(Integer position, SdJwtSalt salt) {
SdJwtSalt sdJwtSalt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt;
DecoyArrayElement decoyElement = DecoyArrayElement.builder().withSalt(sdJwtSalt).atIndex(position).build();
this.decoyElements.add(decoyElement);
}
public ArrayDisclosure build() {
return new ArrayDisclosure(claimName, Collections.unmodifiableList(elements),
Collections.unmodifiableList(decoyElements));
}
}
public static Builder builder() {
return new Builder();
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class DecoyArrayElement extends DecoyEntry {
private final Integer index;
private DecoyArrayElement(SdJwtSalt salt, Integer index) {
super(salt);
this.index = index;
}
public JsonNode getVisibleValue(String hashAlg) {
return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg));
}
public Integer getIndex() {
return index;
}
public static class Builder {
private SdJwtSalt salt;
private Integer index;
public Builder withSalt(SdJwtSalt salt) {
this.salt = salt;
return this;
}
public Builder atIndex(Integer index) {
this.index = index;
return this;
}
public DecoyArrayElement build() {
salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt;
return new DecoyArrayElement(salt, index);
}
}
public static Builder builder() {
return new Builder();
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class DecoyClaim extends DecoyEntry {
private DecoyClaim(SdJwtSalt salt) {
super(salt);
}
public static class Builder {
private SdJwtSalt salt;
public Builder withSalt(SdJwtSalt salt) {
this.salt = salt;
return this;
}
public DecoyClaim build() {
salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt;
return new DecoyClaim(salt);
}
}
public static Builder builder() {
return new Builder();
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Objects;
import org.keycloak.jose.jws.crypto.HashUtils;
/**
* Handles hash production for a decoy entry from the given salt.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public abstract class DecoyEntry {
private final SdJwtSalt salt;
protected DecoyEntry(SdJwtSalt salt) {
this.salt = Objects.requireNonNull(salt, "DecoyEntry always requires a non null salt");
}
public SdJwtSalt getSalt() {
return salt;
}
public String getDisclosureDigest(String hashAlg) {
return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, salt.toString().getBytes()));
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Objects;
import org.keycloak.jose.jws.crypto.HashUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
/**
* Handles undisclosed claims and array elements, providing functionality
* to generate disclosure digests from Base64Url encoded strings.
*
* Hiding claims and array elements occurs by including their digests
* instead of plaintext in the signed verifiable credential.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public abstract class Disclosable {
private final SdJwtSalt salt;
/**
* Returns the array of undisclosed value, for
* encoding (disclosure string) and hashing (_sd digest array in the VC).
*/
abstract Object[] toArray();
protected Disclosable(SdJwtSalt salt) {
this.salt = Objects.requireNonNull(salt, "Disclosure always requires a salt must not be null");
}
public SdJwtSalt getSalt() {
return salt;
}
public String getSaltAsString() {
return salt.toString();
}
public String toJson() {
try {
return SdJwtUtils.printJsonArray(toArray());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public String getDisclosureString() {
String json = toJson();
return SdJwtUtils.encodeNoPad(json.getBytes());
}
public String getDisclosureDigest(String hashAlg) {
return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, getDisclosureString().getBytes()));
}
@Override
public String toString() {
return getDisclosureString();
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class DisclosureRedList {
public static final List<String> redList = Collections
.unmodifiableList(Arrays.asList("iss", "iat", "nbf", "exp", "cnf", "vct", "status"));
private final Set<SdJwtClaimName> redListClaimNames;
public static final DisclosureRedList defaultList = defaultList();
public DisclosureRedList of(Set<SdJwtClaimName> redListClaimNames) {
return new DisclosureRedList(redListClaimNames);
}
private static DisclosureRedList defaultList() {
return new DisclosureRedList(redList.stream().map(SdJwtClaimName::of).collect(Collectors.toSet()));
}
private DisclosureRedList(Set<SdJwtClaimName> redListClaimNames) {
this.redListClaimNames = Collections.unmodifiableSet(redListClaimNames);
}
public boolean isRedListedClaimName(SdJwtClaimName claimName) {
return redListClaimNames.contains(claimName);
}
public boolean containsRedListedClaimNames(Collection<SdJwtClaimName> claimNames) {
return !redListClaimNames.isEmpty() && !claimNames.isEmpty()
&& !Collections.disjoint(redListClaimNames, claimNames);
}
}

View file

@ -0,0 +1,191 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Manages the specification of undisclosed claims and array elements.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class DisclosureSpec {
// Map of undisclosed claims and corresponding salt.
// salt can be null;
private final Map<SdJwtClaimName, DisclosureData> undisclosedClaims;
// List of decoy claim. Digest will be produced from disclosure data (salt)
private final List<DisclosureData> decoyClaims;
// Key is the claim name, value is the list of undisclosed elements
private final Map<SdJwtClaimName, Map<Integer, DisclosureData>> undisclosedArrayElts;
// Key is the claim name, value is the list of decoy elements
// Digest will be produced from disclosure data (salt)
private final Map<SdJwtClaimName, Map<Integer, DisclosureData>> decoyArrayElts;
private DisclosureSpec(Map<SdJwtClaimName, DisclosureData> undisclosedClaims,
List<DisclosureData> decoyClaims,
Map<SdJwtClaimName, Map<Integer, DisclosureData>> undisclosedArrayElts,
Map<SdJwtClaimName, Map<Integer, DisclosureData>> decoyArrayElts) {
this.undisclosedClaims = undisclosedClaims;
this.decoyClaims = decoyClaims;
this.undisclosedArrayElts = undisclosedArrayElts;
this.decoyArrayElts = decoyArrayElts;
}
public Map<Integer, DisclosureData> getUndisclosedArrayElts(SdJwtClaimName arrayClaimName) {
return undisclosedArrayElts.get(arrayClaimName);
}
public Map<Integer, DisclosureData> getDecoyArrayElts(SdJwtClaimName arrayClaimName) {
return decoyArrayElts.get(arrayClaimName);
}
public Map<SdJwtClaimName, DisclosureData> getUndisclosedClaims() {
return undisclosedClaims;
}
public List<DisclosureData> getDecoyClaims() {
return decoyClaims;
}
// check if a claim is undisclosed
public DisclosureData getUndisclosedClaim(SdJwtClaimName claimName) {
return undisclosedClaims.get(claimName);
}
// test is claim has undisclosed array elements
public boolean hasUndisclosedArrayElts(SdJwtClaimName claimName) {
return undisclosedArrayElts.containsKey(claimName);
}
public static class Builder {
private final Map<SdJwtClaimName, DisclosureData> undisclosedClaims = new HashMap<>();
private final List<DisclosureData> decoyClaims = new ArrayList<>();
private final Map<SdJwtClaimName, Map<Integer, DisclosureData>> undisclosedArrayElts = new HashMap<>();
private final Map<SdJwtClaimName, Map<Integer, DisclosureData>> decoyArrayElts = new HashMap<>();
private DisclosureRedList redListedClaimNames;
public Builder withUndisclosedClaim(String claimName, String salt) {
this.undisclosedClaims.put(SdJwtClaimName.of(claimName), DisclosureData.of(salt));
return this;
}
public Builder withUndisclosedClaim(String claimName) {
return withUndisclosedClaim(claimName, null);
}
public Builder withDecoyClaim(String salt) {
this.decoyClaims.add(DisclosureData.of(salt));
return this;
}
public Builder withUndisclosedArrayElt(String claimName, Integer undisclosedEltIndex, String salt) {
Map<Integer, DisclosureData> indexes = this.undisclosedArrayElts.computeIfAbsent(
SdJwtClaimName.of(claimName),
k -> new HashMap<>());
indexes.put(undisclosedEltIndex, DisclosureData.of(salt));
return this;
}
public Builder withDecoyArrayElt(String claimName, Integer decoyEltIndex, String salt) {
Map<Integer, DisclosureData> indexes = this.decoyArrayElts.computeIfAbsent(SdJwtClaimName.of(claimName),
k -> new HashMap<>());
indexes.put(decoyEltIndex, DisclosureData.of(salt));
return this;
}
public Builder withRedListedClaimNames(DisclosureRedList redListedClaimNames) {
this.redListedClaimNames = redListedClaimNames;
return this;
}
public DisclosureSpec build() {
// Validate redlist
validateRedList();
Map<SdJwtClaimName, Map<Integer, DisclosureData>> undisclosedArrayEltMap = new HashMap<>();
undisclosedArrayElts.forEach((k, v) -> {
undisclosedArrayEltMap.put(k, Collections.unmodifiableMap((v)));
});
Map<SdJwtClaimName, Map<Integer, DisclosureData>> decoyArrayEltMap = new HashMap<>();
decoyArrayElts.forEach((k, v) -> {
decoyArrayEltMap.put(k, Collections.unmodifiableMap((v)));
});
return new DisclosureSpec(Collections.unmodifiableMap(undisclosedClaims),
Collections.unmodifiableList(decoyClaims),
Collections.unmodifiableMap(undisclosedArrayEltMap),
Collections.unmodifiableMap(decoyArrayEltMap));
}
private void validateRedList() {
// Work with default if none set.
if (redListedClaimNames == null) {
redListedClaimNames = DisclosureRedList.defaultList;
}
// Validate undisclosed claims
if (redListedClaimNames.containsRedListedClaimNames(undisclosedClaims.keySet())) {
throw new IllegalArgumentException("UndisclosedClaims contains red listed claim names");
}
// Validate undisclosed array claims
if (redListedClaimNames.containsRedListedClaimNames(undisclosedArrayElts.keySet())) {
throw new IllegalArgumentException("UndisclosedArrays with red listed claim names");
}
// Validate undisclosed claims
if (redListedClaimNames.containsRedListedClaimNames(decoyArrayElts.keySet())) {
throw new IllegalArgumentException("decoyArrayElts contains red listed claim names");
}
}
}
public static Builder builder() {
return new Builder();
}
public static class DisclosureData {
private final SdJwtSalt salt;
private DisclosureData() {
this.salt = null;
}
private DisclosureData(String salt) {
this.salt = salt == null ? null : SdJwtSalt.of(salt);
}
public static DisclosureData of(String salt) {
return salt == null ? new DisclosureData() : new DisclosureData(salt);
}
public SdJwtSalt getSalt() {
return salt;
}
}
}

View file

@ -0,0 +1,199 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.JWSInput;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Handle verifiable credentials (SD-JWT VC), enabling the parsing
* of existing VCs as well as the creation and signing of new ones.
* It integrates with Keycloak's SignatureSignerContext to facilitate
* the generation of issuer signature.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class IssuerSignedJWT extends SdJws {
public static IssuerSignedJWT fromJws(String jwsString) {
return new IssuerSignedJWT(jwsString);
}
public IssuerSignedJWT toSignedJWT(SignatureSignerContext signer, String jwsType) {
JWSInput jwsInput = sign(getPayload(), signer, jwsType);
return new IssuerSignedJWT(getPayload(), jwsInput);
}
private IssuerSignedJWT(String jwsString) {
super(jwsString);
}
private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
boolean nestedDisclosures) {
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures));
}
private IssuerSignedJWT(JsonNode payload, JWSInput jwsInput) {
super(payload, jwsInput);
}
private IssuerSignedJWT(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
boolean nestedDisclosures, SignatureSignerContext signer, String jwsType) {
super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, jwsType);
}
/*
* Generates the payload of the issuer signed jwt from the list
* of claims.
*/
private static JsonNode generatePayloadString(List<SdJwtClaim> claims, List<DecoyClaim> decoyClaims, String hashAlg,
boolean nestedDisclosures) {
SdJwtUtils.requireNonEmpty(hashAlg, "hashAlg must not be null or empty");
final List<SdJwtClaim> claimsInternal = claims == null ? Collections.emptyList()
: Collections.unmodifiableList(claims);
final List<DecoyClaim> decoyClaimsInternal = decoyClaims == null ? Collections.emptyList()
: Collections.unmodifiableList(decoyClaims);
try {
// Check no dupplicate claim names
claimsInternal.stream()
.filter(Objects::nonNull)
// is any duplicate, toMap will throw IllegalStateException
.collect(Collectors.toMap(SdJwtClaim::getClaimName, claim -> claim));
} catch (IllegalStateException e) {
throw new IllegalArgumentException("claims must not contain duplicate claim names", e);
}
ArrayNode sdArray = SdJwtUtils.mapper.createArrayNode();
// first filter all UndisclosedClaim
// then sort by salt
// then push digest into the sdArray
List<String> digests = claimsInternal.stream()
.filter(claim -> claim instanceof UndisclosedClaim)
.map(claim -> (UndisclosedClaim) claim)
.collect(Collectors.toMap(UndisclosedClaim::getSalt, claim -> claim))
.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.filter(Objects::nonNull)
.map(od -> od.getDisclosureDigest(hashAlg))
.collect(Collectors.toList());
// add decoy claims
decoyClaimsInternal.stream().map(claim -> claim.getDisclosureDigest(hashAlg)).forEach(digests::add);
digests.stream().sorted().forEach(sdArray::add);
ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
if (sdArray.size() > 0) {
// drop _sd claim if empty
payload.set(CLAIM_NAME_SELECTIVE_DISCLOSURE, sdArray);
}
if (sdArray.size() > 0 || nestedDisclosures) {
// add sd alg only if ay disclosure.
payload.put(CLAIM_NAME_SD_HASH_ALGORITHM, hashAlg);
}
// then put all other claims in the paypload
// Disclosure of array of elements is handled
// by the corresponding claim object.
claimsInternal.stream()
.filter(Objects::nonNull)
.filter(claim -> !(claim instanceof UndisclosedClaim))
.forEach(nullableClaim -> {
SdJwtClaim claim = Objects.requireNonNull(nullableClaim);
payload.set(claim.getClaimNameAsString(), claim.getVisibleClaimValue(hashAlg));
});
return payload;
}
// SD-JWT Claims
public static final String CLAIM_NAME_SELECTIVE_DISCLOSURE = "_sd";
public static final String CLAIM_NAME_SD_HASH_ALGORITHM = "_sd_alg";
// Builder
public static Builder builder() {
return new Builder();
}
public static class Builder {
private List<SdJwtClaim> claims;
private String hashAlg;
private SignatureSignerContext signer;
private List<DecoyClaim> decoyClaims;
private boolean nestedDisclosures;
private String jwsType = "vc+sd-jwt";
public Builder withClaims(List<SdJwtClaim> claims) {
this.claims = claims;
return this;
}
public Builder withDecoyClaims(List<DecoyClaim> decoyClaims) {
this.decoyClaims = decoyClaims;
return this;
}
public Builder withHashAlg(String hashAlg) {
this.hashAlg = hashAlg;
return this;
}
public Builder withSigner(SignatureSignerContext signer) {
this.signer = signer;
return this;
}
public Builder withNestedDisclosures(boolean nestedDisclosures) {
this.nestedDisclosures = nestedDisclosures;
return this;
}
public Builder withJwsType(String jwsType) {
this.jwsType = jwsType;
return this;
}
public IssuerSignedJWT build() {
// Preinitialize hashAlg to sha-256 if not provided
hashAlg = hashAlg == null ? "sha-256" : hashAlg;
// send an empty lise if claims not set.
claims = claims == null ? Collections.emptyList() : claims;
decoyClaims = decoyClaims == null ? Collections.emptyList() : decoyClaims;
if (signer != null) {
return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures, signer, jwsType);
} else {
return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures);
}
}
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.io.IOException;
import java.util.Objects;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Handle jws, either the issuer jwt or the holder key binding jwt.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class SdJws {
private final JWSInput jwsInput;
private final JsonNode payload;
public String toJws() {
if (jwsInput == null) {
throw new IllegalStateException("JWS not yet signed");
}
return jwsInput.getWireString();
}
public JsonNode getPayload() {
return payload;
}
public String getJwsString() {
return jwsInput.getWireString();
}
// Constructor for unsigned JWS
protected SdJws(JsonNode payload) {
this.payload = payload;
this.jwsInput = null;
}
// Constructor from jws string with all parts
protected SdJws(String jwsString) {
this.jwsInput = parse(jwsString);
this.payload = readPayload(jwsInput);
}
// Constructor for signed JWS
protected SdJws(JsonNode payload, JWSInput jwsInput) {
this.payload = payload;
this.jwsInput = jwsInput;
}
protected SdJws(JsonNode payload, SignatureSignerContext signer, String jwsType) {
this.payload = payload;
this.jwsInput = sign(payload, signer, jwsType);
}
protected static JWSInput sign(JsonNode payload, SignatureSignerContext signer, String jwsType) {
String jwsString = new JWSBuilder().type(jwsType).jsonContent(payload).sign(signer);
return parse(jwsString);
}
public void verifySignature(SignatureVerifierContext verifier) throws VerificationException {
Objects.requireNonNull(verifier, "verifier must not be null");
try {
if (!verifier.verify(jwsInput.getEncodedSignatureInput().getBytes("UTF-8"), jwsInput.getSignature())) {
throw new VerificationException("Invalid jws signature");
}
} catch (Exception e) {
throw new VerificationException(e);
}
}
private static final JWSInput parse(String jwsString) {
try {
return new JWSInput(Objects.requireNonNull(jwsString, "jwsString must not be null"));
} catch (JWSInputException e) {
throw new RuntimeException(e);
}
}
private static final JsonNode readPayload(JWSInput jwsInput) {
try {
return SdJwtUtils.mapper.readTree(jwsInput.getContent());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.sdjwt.vp.KeyBindingJWT;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Main entry class for selective disclosure jwt (SD-JWT).
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class SdJwt {
public static final String DELIMITER = "~";
private final IssuerSignedJWT issuerSignedJWT;
private final List<SdJwtClaim> claims;
private final List<String> disclosures = new ArrayList<>();
private Optional<String> sdJwtString = Optional.empty();
private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List<SdJwt> nesteSdJwts,
Optional<KeyBindingJWT> keyBindingJWT,
SignatureSignerContext signer) {
claims = new ArrayList<>();
claimSet.fields()
.forEachRemaining(entry -> claims.add(createClaim(entry.getKey(), entry.getValue(), disclosureSpec)));
this.issuerSignedJWT = IssuerSignedJWT.builder()
.withClaims(claims)
.withDecoyClaims(createdDecoyClaims(disclosureSpec))
.withNestedDisclosures(!nesteSdJwts.isEmpty())
.withSigner(signer)
.build();
nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures()));
this.disclosures.addAll(getDisclosureStrings(claims));
}
private List<DecoyClaim> createdDecoyClaims(DisclosureSpec disclosureSpec) {
return disclosureSpec.getDecoyClaims().stream()
.map(disclosureData -> DecoyClaim.builder().withSalt(disclosureData.getSalt()).build())
.collect(Collectors.toList());
}
/**
* Prepare to a nested payload to this SD-JWT.
*
* droping the algo claim.
*
* @param nestedSdJwt
* @return
*/
public JsonNode asNestedPayload() {
JsonNode nestedPayload = issuerSignedJWT.getPayload();
((ObjectNode) nestedPayload).remove(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM);
return nestedPayload;
}
public String toSdJwtString() {
List<String> parts = new ArrayList<>();
parts.add(issuerSignedJWT.toJws());
parts.addAll(disclosures);
parts.add("");
return String.join(DELIMITER, parts);
}
private static List<String> getDisclosureStrings(List<SdJwtClaim> claims) {
List<String> disclosureStrings = new ArrayList<>();
claims.stream()
.map(SdJwtClaim::getDisclosureStrings)
.forEach(disclosureStrings::addAll);
return Collections.unmodifiableList(disclosureStrings);
}
@Override
public String toString() {
return sdJwtString.orElseGet(() -> {
String sdString = toSdJwtString();
sdJwtString = Optional.of(sdString);
return sdString;
});
}
private SdJwtClaim createClaim(String claimName, JsonNode claimValue, DisclosureSpec disclosureSpec) {
DisclosureSpec.DisclosureData disclosureData = disclosureSpec.getUndisclosedClaim(SdJwtClaimName.of(claimName));
if (disclosureData != null) {
return createUndisclosedClaim(claimName, claimValue, disclosureData.getSalt());
} else {
return createArrayOrVisibleClaim(claimName, claimValue, disclosureSpec);
}
}
private SdJwtClaim createUndisclosedClaim(String claimName, JsonNode claimValue, SdJwtSalt salt) {
return UndisclosedClaim.builder()
.withClaimName(claimName)
.withClaimValue(claimValue)
.withSalt(salt)
.build();
}
private SdJwtClaim createArrayOrVisibleClaim(String claimName, JsonNode claimValue, DisclosureSpec disclosureSpec) {
SdJwtClaimName sdJwtClaimName = SdJwtClaimName.of(claimName);
Map<Integer, DisclosureSpec.DisclosureData> undisclosedArrayElts = disclosureSpec
.getUndisclosedArrayElts(sdJwtClaimName);
Map<Integer, DisclosureSpec.DisclosureData> decoyArrayElts = disclosureSpec.getDecoyArrayElts(sdJwtClaimName);
if (undisclosedArrayElts != null || decoyArrayElts != null) {
return createArrayDisclosure(claimName, claimValue, undisclosedArrayElts, decoyArrayElts);
} else {
return VisibleSdJwtClaim.builder()
.withClaimName(claimName)
.withClaimValue(claimValue)
.build();
}
}
private SdJwtClaim createArrayDisclosure(String claimName, JsonNode claimValue,
Map<Integer, DisclosureSpec.DisclosureData> undisclosedArrayElts,
Map<Integer, DisclosureSpec.DisclosureData> decoyArrayElts) {
ArrayNode arrayNode = validateArrayNode(claimName, claimValue);
ArrayDisclosure.Builder arrayDisclosureBuilder = ArrayDisclosure.builder().withClaimName(claimName);
if (undisclosedArrayElts != null) {
IntStream.range(0, arrayNode.size())
.forEach(i -> processArrayElement(arrayDisclosureBuilder, arrayNode.get(i),
undisclosedArrayElts.get(i)));
}
if (decoyArrayElts != null) {
decoyArrayElts.entrySet().stream()
.forEach(e -> arrayDisclosureBuilder.withDecoyElt(e.getKey(), e.getValue().getSalt()));
}
return arrayDisclosureBuilder.build();
}
private ArrayNode validateArrayNode(String claimName, JsonNode claimValue) {
return Optional.of(claimValue)
.filter(v -> v.getNodeType() == JsonNodeType.ARRAY)
.map(v -> (ArrayNode) v)
.orElseThrow(
() -> new IllegalArgumentException("Expected array for claim with name: " + claimName));
}
private void processArrayElement(ArrayDisclosure.Builder builder, JsonNode elementValue,
DisclosureSpec.DisclosureData disclosureData) {
if (disclosureData != null) {
builder.withUndisclosedElement(disclosureData.getSalt(), elementValue);
} else {
builder.withVisibleElement(elementValue);
}
}
public IssuerSignedJWT getIssuerSignedJWT() {
return issuerSignedJWT;
}
public List<String> getDisclosures() {
return disclosures;
}
// builder for SdJwt
public static class Builder {
private DisclosureSpec disclosureSpec;
private JsonNode claimSet;
private Optional<KeyBindingJWT> keyBindingJWT = Optional.empty();
private SignatureSignerContext signer;
private final List<SdJwt> nestedSdJwts = new ArrayList<>();
public Builder withDisclosureSpec(DisclosureSpec disclosureSpec) {
this.disclosureSpec = disclosureSpec;
return this;
}
public Builder withClaimSet(JsonNode claimSet) {
this.claimSet = claimSet;
return this;
}
public Builder withKeyBindingJWT(KeyBindingJWT keyBindingJWT) {
this.keyBindingJWT = Optional.of(keyBindingJWT);
return this;
}
public Builder withSigner(SignatureSignerContext signer) {
this.signer = signer;
return this;
}
public Builder withNestedSdJwt(SdJwt nestedSdJwt) {
nestedSdJwts.add(nestedSdJwt);
return this;
}
public SdJwt build() {
return new SdJwt(disclosureSpec, claimSet, nestedSdJwts, keyBindingJWT, signer);
}
}
public static Builder builder() {
return new Builder();
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public interface SdJwtArrayElement {
/**
* Returns the value visibly printed as array element
* in the issuer signed jwt.
*/
public JsonNode getVisibleValue(String hashAlg);
public String getDisclosureString();
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Represents a top level claim in the payload of a JWT.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public interface SdJwtClaim {
public SdJwtClaimName getClaimName();
public String getClaimNameAsString();
public JsonNode getVisibleClaimValue(String hashAlgo);
public List<String> getDisclosureStrings();
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
/**
* Strong typing claim name to avoid parameter mismatch.
*
* Used as map key. Beware of the hashcode and equals implementation.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtClaimName {
private final String claimName;
public SdJwtClaimName(String claimName) {
this.claimName = SdJwtUtils.requireNonEmpty(claimName, "claimName must not be empty");
}
public static SdJwtClaimName of(String claimName) {
return new SdJwtClaimName(claimName);
}
@Override
public String toString() {
return claimName;
}
@Override
public int hashCode() {
return claimName.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SdJwtClaimName) {
return claimName.equals(((SdJwtClaimName) obj).claimName);
}
return false;
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
/**
* Strong typing salt to avoid parameter mismatch.
*
* Comparable to allow sorting in SD-JWT VC.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtSalt implements Comparable<SdJwtSalt> {
private final String salt;
public SdJwtSalt(String salt) {
this.salt = SdJwtUtils.requireNonEmpty(salt, "salt must not be empty");
}
// Handy factory method
public static SdJwtSalt of(String salt) {
return new SdJwtSalt(salt);
}
@Override
public String toString() {
return salt;
}
@Override
public int compareTo(SdJwtSalt o) {
return salt.compareTo(o.salt);
}
}

View file

@ -0,0 +1,107 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Optional;
import org.keycloak.common.util.Base64Url;
import org.keycloak.jose.jws.crypto.HashUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtUtils {
public static final ObjectMapper mapper = new ObjectMapper();
private static SecureRandom RANDOM = new SecureRandom();
public static String encodeNoPad(byte[] bytes) {
return Base64Url.encode(bytes);
}
public static String hashAndBase64EncodeNoPad(byte[] disclosureBytes, String hashAlg) {
return encodeNoPad(HashUtils.hash(hashAlg, disclosureBytes));
}
public static String requireNonEmpty(String str, String message) {
return Optional.ofNullable(str)
.filter(s -> !s.isEmpty())
.orElseThrow(() -> new IllegalArgumentException(message));
}
public static String randomSalt() {
// 16 bytes for 128-bit entropy.
// Base64url-encoded
return encodeNoPad(randomBytes(16));
}
public static byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
RANDOM.nextBytes(bytes);
return bytes;
}
public static String printJsonArray(Object[] array) throws JsonProcessingException {
if (arrayEltSpaced) {
return arraySpacedPrettyPrinter.writer.writeValueAsString(array);
} else {
return mapper.writeValueAsString(array);
}
}
static ArraySpacedPrettyPrinter arraySpacedPrettyPrinter = new ArraySpacedPrettyPrinter();
static class ArraySpacedPrettyPrinter extends MinimalPrettyPrinter {
final ObjectMapper prettyPrinObjectMapper;
final ObjectWriter writer;
public ArraySpacedPrettyPrinter() {
prettyPrinObjectMapper = new ObjectMapper();
prettyPrinObjectMapper.setDefaultPrettyPrinter(this);
writer = prettyPrinObjectMapper.writer(this);
}
@Override
public void writeArrayValueSeparator(JsonGenerator jg) throws IOException {
jg.writeRaw(',');
jg.writeRaw(' ');
}
@Override
public void writeObjectEntrySeparator(JsonGenerator jg) throws IOException {
jg.writeRaw(',');
jg.writeRaw(' '); // Add a space after comma
}
@Override
public void writeObjectFieldValueSeparator(JsonGenerator jg) throws IOException {
jg.writeRaw(':');
jg.writeRaw(' '); // Add a space after comma
}
}
public static boolean arrayEltSpaced = true;
}

View file

@ -0,0 +1,69 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Objects;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayElement {
private final JsonNode arrayElement;
private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) {
super(salt);
this.arrayElement = arrayElement;
}
@Override
public JsonNode getVisibleValue(String hashAlg) {
return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg));
}
@Override
Object[] toArray() {
return new Object[] { getSaltAsString(), arrayElement };
}
public static class Builder {
private SdJwtSalt salt;
private JsonNode arrayElement;
public Builder withSalt(SdJwtSalt salt) {
this.salt = salt;
return this;
}
public Builder withArrayElement(JsonNode arrayElement) {
this.arrayElement = arrayElement;
return this;
}
public UndisclosedArrayElement build() {
arrayElement = Objects.requireNonNull(arrayElement, "arrayElement must not be null");
salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt;
return new UndisclosedArrayElement(salt, arrayElement);
}
}
public static Builder builder() {
return new Builder();
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class UndisclosedClaim extends Disclosable implements SdJwtClaim {
private final SdJwtClaimName claimName;
private final JsonNode claimValue;
private UndisclosedClaim(SdJwtClaimName claimName, SdJwtSalt salt, JsonNode claimValue) {
super(salt);
this.claimName = claimName;
this.claimValue = claimValue;
}
@Override
Object[] toArray() {
return new Object[] { getSaltAsString(), getClaimNameAsString(), claimValue };
}
@Override
public SdJwtClaimName getClaimName() {
return claimName;
}
@Override
public String getClaimNameAsString() {
return claimName.toString();
}
/**
* Recall no info is visible on these claims in the JWT.
*/
@Override
public JsonNode getVisibleClaimValue(String hashAlgo) {
throw new UnsupportedOperationException("Unimplemented method 'getVisibleClaimValue'");
}
public static class Builder {
private SdJwtClaimName claimName;
private SdJwtSalt salt;
private JsonNode claimValue;
public Builder withClaimName(String claimName) {
this.claimName = new SdJwtClaimName(claimName);
return this;
}
public Builder withSalt(SdJwtSalt salt) {
this.salt = salt;
return this;
}
public Builder withClaimValue(JsonNode claimValue) {
this.claimValue = claimValue;
return this;
}
public UndisclosedClaim build() {
claimName = Objects.requireNonNull(claimName, "claimName must not be null");
claimValue = Objects.requireNonNull(claimValue, "claimValue must not be null");
salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt;
return new UndisclosedClaim(claimName, salt, claimValue);
}
}
public static Builder builder() {
return new Builder();
}
@Override
public List<String> getDisclosureStrings() {
return Collections.singletonList(getDisclosureString());
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class VisibleArrayElement implements SdJwtArrayElement {
private final JsonNode arrayElement;
public VisibleArrayElement(JsonNode arrayElement) {
this.arrayElement = arrayElement;
}
@Override
public JsonNode getVisibleValue(String hashAlg) {
return arrayElement;
}
@Override
public String getDisclosureString() {
return null;
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class VisibleSdJwtClaim extends AbstractSdJwtClaim {
private final JsonNode claimValue;
public VisibleSdJwtClaim(SdJwtClaimName claimName, JsonNode claimValue) {
super(claimName);
this.claimValue = claimValue;
}
@Override
public JsonNode getVisibleClaimValue(String hashAlgo) {
return claimValue;
}
// Static method to create a builder instance
public static Builder builder() {
return new Builder();
}
// Static inner Builder class
public static class Builder {
private SdJwtClaimName claimName;
private JsonNode claimValue;
public Builder withClaimName(String claimName) {
this.claimName = new SdJwtClaimName(claimName);
return this;
}
public Builder withClaimValue(JsonNode claimValue) {
this.claimValue = claimValue;
return this;
}
public VisibleSdJwtClaim build() {
claimName = Objects.requireNonNull(claimName, "claimName must not be null");
claimValue = Objects.requireNonNull(claimValue, "claimValue must not be null");
return new VisibleSdJwtClaim(claimName, claimValue);
}
}
@Override
public List<String> getDisclosureStrings() {
return Collections.emptyList();
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt.vp;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.sdjwt.SdJws;
import com.fasterxml.jackson.databind.JsonNode;
/**
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class KeyBindingJWT extends SdJws {
public static KeyBindingJWT of(String jwsString) {
return new KeyBindingJWT(jwsString);
}
public static KeyBindingJWT from(JsonNode payload, SignatureSignerContext signer, String jwsType) {
JWSInput jwsInput = sign(payload, signer, jwsType);
return new KeyBindingJWT(payload, jwsInput);
}
private KeyBindingJWT(JsonNode payload, JWSInput jwsInput) {
super(payload, jwsInput);
}
private KeyBindingJWT(String jwsString) {
super(jwsString);
}
}

View file

@ -0,0 +1,265 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt.vp;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.keycloak.common.util.Base64Url;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.sdjwt.IssuerSignedJWT;
import org.keycloak.sdjwt.SdJwt;
import org.keycloak.sdjwt.SdJwtUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtVP {
private String sdJwtVpString;
private final IssuerSignedJWT issuerSignedJWT;
private final Map<String, ArrayNode> claims;
private final Map<String, String> disclosures;
private final Map<String, String> recursiveDigests;
private final List<String> ghostDigests;
private final String hashAlgorithm;
private final Optional<KeyBindingJWT> keyBindingJWT;
public IssuerSignedJWT getIssuerSignedJWT() {
return issuerSignedJWT;
}
public Map<String, String> getDisclosures() {
return disclosures;
}
public Collection<String> getDisclosuresString() {
return disclosures.values();
}
public Map<String, String> getRecursiveDigests() {
return recursiveDigests;
}
public Collection<String> getGhostDigests() {
return ghostDigests;
}
public String getHashAlgorithm() {
return hashAlgorithm;
}
public Optional<KeyBindingJWT> getKeyBindingJWT() {
return keyBindingJWT;
}
private SdJwtVP(String sdJwtVpString, String hashAlgorithm, IssuerSignedJWT issuerSignedJWT,
Map<String, ArrayNode> claims, Map<String, String> disclosures, Map<String, String> recursiveDigests,
List<String> ghostDigests, Optional<KeyBindingJWT> keyBindingJWT) {
this.sdJwtVpString = sdJwtVpString;
this.hashAlgorithm = hashAlgorithm;
this.issuerSignedJWT = issuerSignedJWT;
this.claims = Collections.unmodifiableMap(claims);
this.disclosures = Collections.unmodifiableMap(disclosures);
this.recursiveDigests = Collections.unmodifiableMap(recursiveDigests);
this.ghostDigests = Collections.unmodifiableList(ghostDigests);
this.keyBindingJWT = keyBindingJWT;
}
public static SdJwtVP of(String sdJwtString) {
int disclosureStart = sdJwtString.indexOf(SdJwt.DELIMITER);
int disclosureEnd = sdJwtString.lastIndexOf(SdJwt.DELIMITER);
String issuerSignedJWTString = sdJwtString.substring(0, disclosureStart);
String disclosuresString = sdJwtString.substring(disclosureStart + 1, disclosureEnd);
IssuerSignedJWT issuerSignedJWT = IssuerSignedJWT.fromJws(issuerSignedJWTString);
ObjectNode issuerPayload = (ObjectNode) issuerSignedJWT.getPayload();
String hashAlgorithm = issuerPayload.get(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM).asText();
Map<String, ArrayNode> claims = new HashMap<>();
Map<String, String> disclosures = new HashMap<>();
String[] split = disclosuresString.split(SdJwt.DELIMITER);
for (String disclosure : split) {
String disclosureDigest = SdJwtUtils.hashAndBase64EncodeNoPad(disclosure.getBytes(), hashAlgorithm);
if (disclosures.containsKey(disclosureDigest)) {
throw new IllegalArgumentException("Duplicate disclosure digest");
}
disclosures.put(disclosureDigest, disclosure);
ArrayNode disclosureData;
try {
disclosureData = (ArrayNode) SdJwtUtils.mapper.readTree(Base64Url.decode(disclosure));
claims.put(disclosureDigest, disclosureData);
} catch (IOException e) {
throw new IllegalArgumentException("Invalid disclosure data");
}
}
Set<String> allDigests = claims.keySet();
Map<String, String> recursiveDigests = new HashMap<>();
List<String> ghostDigests = new ArrayList<>();
allDigests.stream()
.forEach(disclosureDigest -> {
JsonNode node = findNode(issuerPayload, disclosureDigest);
node = processDisclosureDigest(node, disclosureDigest, claims, recursiveDigests, ghostDigests);
});
Optional<KeyBindingJWT> keyBindingJWT = Optional.empty();
if (sdJwtString.length() > disclosureEnd + 1) {
String keyBindingJWTString = sdJwtString.substring(disclosureEnd + 1);
keyBindingJWT = Optional.of(KeyBindingJWT.of(keyBindingJWTString));
}
// Drop the key binding String if any. As it is held by the keyBindingJwtObject
String sdJWtVPString = sdJwtString.substring(0, disclosureEnd + 1);
return new SdJwtVP(sdJWtVPString, hashAlgorithm, issuerSignedJWT, claims, disclosures, recursiveDigests,
ghostDigests, keyBindingJWT);
}
private static JsonNode processDisclosureDigest(JsonNode node, String disclosureDigest,
Map<String, ArrayNode> claims,
Map<String, String> recursiveDigests,
List<String> ghostDigests) {
if (node == null) { // digest is nested in another disclosure
Set<Entry<String, ArrayNode>> entrySet = claims.entrySet();
for (Entry<String, ArrayNode> entry : entrySet) {
if (entry.getKey().equals(disclosureDigest)) {
continue;
}
node = findNode(entry.getValue(), disclosureDigest);
if (node != null) {
recursiveDigests.put(disclosureDigest, entry.getKey());
break;
}
}
}
if (node == null) { // No digest found for disclosure.
ghostDigests.add(disclosureDigest);
}
return node;
}
public JsonNode getCnfClaim() {
return issuerSignedJWT.getPayload().get("cnf");
}
public String present(List<String> disclosureDigests, JsonNode keyBindingClaims,
SignatureSignerContext holdSignatureSignerContext, String jwsType) {
StringBuilder sb = new StringBuilder();
if (disclosureDigests == null || disclosureDigests.isEmpty()) {
// disclose everything
sb.append(sdJwtVpString);
} else {
sb.append(issuerSignedJWT.toJws());
sb.append(SdJwt.DELIMITER);
for (String disclosureDigest : disclosureDigests) {
sb.append(disclosures.get(disclosureDigest));
sb.append(SdJwt.DELIMITER);
}
}
String unboundPresentation = sb.toString();
if (keyBindingClaims == null || holdSignatureSignerContext == null) {
return unboundPresentation;
}
String sd_hash = SdJwtUtils.hashAndBase64EncodeNoPad(unboundPresentation.getBytes(), getHashAlgorithm());
keyBindingClaims = ((ObjectNode) keyBindingClaims).put("sd_hash", sd_hash);
KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(keyBindingClaims, holdSignatureSignerContext, jwsType);
sb.append(keyBindingJWT.getJwsString());
return sb.toString();
}
// Recursively seraches the node with the given value.
// Returns the node if found, null otherwise.
private static JsonNode findNode(JsonNode node, String value) {
if (node == null) {
return null;
}
if (node.isValueNode()) {
if (node.asText().equals(value)) {
return node;
} else {
return null;
}
}
if (node.isArray() || node.isObject()) {
for (JsonNode child : node) {
JsonNode found = findNode(child, value);
if (found != null) {
return found;
}
}
}
return null;
}
@Override
public String toString() {
return sdJwtVpString;
}
public String verbose() {
StringBuilder sb = new StringBuilder();
sb.append("Issuer Signed JWT: ");
sb.append(issuerSignedJWT.getPayload());
sb.append("\n");
disclosures.forEach((digest, disclosure) -> {
sb.append("\n");
sb.append("Digest: ");
sb.append(digest);
sb.append("\n");
sb.append("Disclosure: ");
sb.append(disclosure);
sb.append("\n");
sb.append("Content: ");
sb.append(claims.get(digest));
sb.append("\n");
});
sb.append("\n");
sb.append("Recursive Digests: ");
sb.append(recursiveDigests);
sb.append("\n");
sb.append("\n");
sb.append("Ghost Digests: ");
sb.append(ghostDigests);
sb.append("\n");
sb.append("\n");
if (keyBindingJWT.isPresent()) {
sb.append("Key Binding JWT: ");
sb.append("\n");
sb.append(keyBindingJWT.get().getPayload().toString());
sb.append("\n");
}
return sb.toString();
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.fasterxml.jackson.databind.JsonNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class ArrayElementDisclosureTest {
@Test
public void testSdJwtWithUndiclosedArrayElements6_1() {
JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json");
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ")
.withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg")
.withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A")
.withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw")
.withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA")
.build();
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(claimSet)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(),
"sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json");
assertEquals(expected, jwt.getPayload());
}
@Test
public void testSdJwtWithUndiclosedAndDecoyArrayElements6_1() {
JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json");
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ")
.withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg")
.withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A")
.withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw")
.withUndisclosedArrayElt("nationalities", 0, "Qg_O64zqAxe412a108iroA")
.withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA")
.withDecoyArrayElt("nationalities", 1, "5bPs1IquZNa0hkaFzzzZNw")
.build();
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(claimSet)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(),
"sdjwt/s6.1-issuer-payload-decoy-array-ellement.json");
assertEquals(expected, jwt.getPayload());
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.fasterxml.jackson.databind.node.TextNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class ArrayElementSerializationTest {
@Before
public void setUp() throws Exception {
SdJwtUtils.arrayEltSpaced = false;
}
@After
public void tearDown() throws Exception {
SdJwtUtils.arrayEltSpaced = true;
}
@Test
public void testToBase64urlEncoded() {
// Create an instance of UndisclosedArrayElement with the specified fields
// "lklxF5jMYlGTPUovMNIvCA", "FR"
UndisclosedArrayElement arrayElementDisclosure = UndisclosedArrayElement.builder()
.withSalt(new SdJwtSalt("lklxF5jMYlGTPUovMNIvCA"))
.withArrayElement(new TextNode("FR")).build();
// Expected Base64 URL encoded string
String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ";
// Assert that the base64 URL encoded string from the object matches the
// expected string
assertEquals(expected, arrayElementDisclosure.getDisclosureString());
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import org.junit.Test;
public class DisclosureRedListTest {
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedInObjectClaim() {
DisclosureSpec.builder()
.withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("vct")
.build();
}
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedInArrayClaim() {
DisclosureSpec.builder()
.withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedArrayElt("iat", 0, "2GLC42sKQveCfGfryNRN9w")
.build();
}
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedInDecoyArrayClaim() {
DisclosureSpec.builder()
.withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w")
.withDecoyArrayElt("exp", 0, "2GLC42sKQveCfGfryNRN9w")
.build();
}
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedIss() {
DisclosureSpec.builder().withUndisclosedClaim("iss").build();
}
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedInObjectNbf() {
DisclosureSpec.builder().withUndisclosedClaim("nbf").build();
}
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedCnf() {
DisclosureSpec.builder().withUndisclosedClaim("cnf").build();
}
@Test(expected = IllegalArgumentException.class)
public void testDefaultRedListedStatus() {
DisclosureSpec.builder().withUndisclosedClaim("status").build();
}
}

View file

@ -0,0 +1,133 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class IssuerSignedJWTTest {
/**
* If issuer decides to disclose everything, paylod of issuer signed JWT should
* be same as the claim set.
*
* This is essential for backward compatibility with non sd based jwt issuance.
*
* @throws IOException
*/
@Test
public void testIssuerSignedJWTPayloadWithValidClaims() {
JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json");
List<SdJwtClaim> claims = new ArrayList<>();
claimSet.fields().forEachRemaining(entry -> {
claims.add(
VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build());
});
IssuerSignedJWT jwt = IssuerSignedJWT.builder().withClaims(claims).build();
assertEquals(claimSet, jwt.getPayload());
}
@Test
public void testIssuerSignedJWTPayloadThrowsExceptionForDuplicateClaims() throws IOException {
JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json");
List<SdJwtClaim> claims = new ArrayList<>();
// First fill claims
claimSet.fields().forEachRemaining(entry -> {
claims.add(
VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build());
});
// First fill claims
claimSet.fields().forEachRemaining(entry -> {
claims.add(
VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build());
});
// All claims are duplicate.
assertTrue(claims.size() == claimSet.size() * 2);
// Expecting exception
assertThrows(IllegalArgumentException.class, () -> IssuerSignedJWT.builder().withClaims(claims).build());
}
@Test
public void testIssuerSignedJWTWithUndiclosedClaims6_1() {
JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json");
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ")
.withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg")
.withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A")
.withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw").build();
SdJwt sdJwt = SdJwt.builder().withDisclosureSpec(disclosureSpec).withClaimSet(claimSet).build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
}
@Test
public void testIssuerSignedJWTWithUndiclosedClaims3_3() {
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A")
.withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA")
.withUndisclosedClaim("phone_number", "eI8ZWm9QnKPpNPeNenHdhQ")
.withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA")
.withUndisclosedClaim("birthdate", "AJx-095VPrpTtN4QMOqROA")
.withUndisclosedClaim("is_over_18", "Pc33JM2LchcU_lHggv_ufQ")
.withUndisclosedClaim("is_over_21", "G02NSrQfjFXQ7Io09syajA")
.withUndisclosedClaim("is_over_65", "lklxF5jMYlGTPUovMNIvCA")
.build();
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json");
// Read claims added by the issuer
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-claims.json");
// Merge both
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class JsonClaimsetTest {
@Test
public void testRead61ClaimSet() throws IOException {
InputStream is = getClass().getClassLoader().getResourceAsStream("sdjwt/s6.1-holder-claims.json");
JsonNode claimSet = SdJwtUtils.mapper.readTree(is);
// Test reading a String
String expected_sub_claim = "user_42";
JsonNode sub_claim = claimSet.get("sub");
assertEquals(JsonNodeType.STRING, sub_claim.getNodeType());
assertEquals(expected_sub_claim, sub_claim.asText());
// Test reading a boolean
JsonNode phone_number_verified_claim = claimSet.get("phone_number_verified");
Boolean expected_phone_number_verified_claim = true;
assertEquals(JsonNodeType.BOOLEAN, phone_number_verified_claim.getNodeType());
assertEquals(expected_phone_number_verified_claim, phone_number_verified_claim.asBoolean());
// Test reading an object
JsonNode address_claim = claimSet.get("address");
assertEquals(JsonNodeType.OBJECT, address_claim.getNodeType());
JsonNode street_address_claim = address_claim.get("street_address");
assertEquals(JsonNodeType.STRING, street_address_claim.getNodeType());
String expected_street_address_claim = "123 Main St";
assertEquals(expected_street_address_claim, street_address_claim.asText());
// Test reading a number
JsonNode updated_at_claim = claimSet.get("updated_at");
int expected_updated_at_claim = 1570000000;
assertEquals(JsonNodeType.NUMBER, updated_at_claim.getNodeType());
assertEquals(expected_updated_at_claim, updated_at_claim.asInt());
// Test reading an array
JsonNode nationalities_claim = claimSet.get("nationalities");
assertEquals(JsonNodeType.ARRAY, nationalities_claim.getNodeType());
assertEquals(2, nationalities_claim.size());
JsonNode element_0_nationalities_claim = nationalities_claim.get(0);
assertEquals(JsonNodeType.STRING, element_0_nationalities_claim.getNodeType());
String expected_element_0_nationalities_claim = "US";
assertEquals(expected_element_0_nationalities_claim, element_0_nationalities_claim.asText());
JsonNode element_1_nationalities_claim = nationalities_claim.get(1);
assertEquals(JsonNodeType.STRING, element_1_nationalities_claim.getNodeType());
String expected_element_1_nationalities_claim = "DE";
assertEquals(expected_element_1_nationalities_claim, element_1_nationalities_claim.asText());
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class JsonNodeComparisonTest {
@Test
public void testJsonNodeEquality() throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode node1 = mapper.readTree("{\"name\":\"John\", \"age\":30}");
JsonNode node2 = mapper.readTree("{\"age\":30, \"name\":\"John\"}");
assertEquals("JsonNode objects should be equal", node1, node2);
}
}

View file

@ -0,0 +1,175 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJWTSamplesTest {
@Test
public void testS7_1_FlatSdJwt() {
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json");
// Read claims added by the issuer
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json");
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
// produce the main sdJwt, adding nested sdJwts
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("address", "2GLC42sKQveCfGfryNRN9w")
.build();
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.1-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
}
@Test
public void testS7_2_StructuredSdJwt() {
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json");
// Read claims added by the issuer
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json");
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A")
.withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA")
.withUndisclosedClaim("country", "eI8ZWm9QnKPpNPeNenHdhQ")
.build();
// Read claims provided by the holder
JsonNode addressClaimSet = holderClaimSet.get("address");
// produce the nested sdJwt
SdJwt addrSdJWT = SdJwt.builder()
.withDisclosureSpec(addrDisclosureSpec)
.withClaimSet(addressClaimSet)
.build();
// cleanup e.g nested _sd_alg
JsonNode addPayload = addrSdJWT.asNestedPayload();
// Set payload back into main claim set
((ObjectNode) holderClaimSet).set("address", addPayload);
DisclosureSpec disclosureSpec = DisclosureSpec.builder().build();
// produce the main sdJwt, adding nested sdJwts
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.withNestedSdJwt(addrSdJWT)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.2-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
}
@Test
public void testS7_2b_PartialDisclosureOfStructuredSdJwt() {
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json");
// Read claims added by the issuer
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json");
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A")
.withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA")
.build();
// Read claims provided by the holder
JsonNode addressClaimSet = holderClaimSet.get("address");
// produce the nested sdJwt
SdJwt addrSdJWT = SdJwt.builder()
.withDisclosureSpec(addrDisclosureSpec)
.withClaimSet(addressClaimSet)
.build();
// cleanup e.g nested _sd_alg
JsonNode addPayload = addrSdJWT.asNestedPayload();
// Set payload back into main claim set
((ObjectNode) holderClaimSet).set("address", addPayload);
DisclosureSpec disclosureSpec = DisclosureSpec.builder().build();
// produce the main sdJwt, adding nested sdJwts
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.withNestedSdJwt(addrSdJWT)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.2b-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
}
@Test
public void testS7_3_RecursiveDisclosureOfStructuredSdJwt() {
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json");
// Read claims added by the issuer
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json");
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A")
.withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA")
.withUndisclosedClaim("country", "eI8ZWm9QnKPpNPeNenHdhQ")
.build();
// Read claims provided by the holder
JsonNode addressClaimSet = holderClaimSet.get("address");
// produce the nested sdJwt
SdJwt addrSdJWT = SdJwt.builder()
.withDisclosureSpec(addrDisclosureSpec)
.withClaimSet(addressClaimSet)
.build();
// cleanup e.g nested _sd_alg
JsonNode addPayload = addrSdJWT.asNestedPayload();
// Set payload back into main claim set
((ObjectNode) holderClaimSet).set("address", addPayload);
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA")
.build();
// produce the main sdJwt, adding nested sdJwts
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.withNestedSdJwt(addrSdJWT)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.3-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
}
}

View file

@ -0,0 +1,105 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.keycloak.crypto.SignatureSignerContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtTest {
@Test
public void settingsTest() {
SignatureSignerContext issuerSignerContext = TestSettings.getInstance().getIssuerSignerContext();
assertNotNull(issuerSignerContext);
}
@Test
public void testA1_Example2_with_nested_disclosure_and_decoy_claims() {
DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("street_address", "AJx-095VPrpTtN4QMOqROA")
.withUndisclosedClaim("locality", "Pc33JM2LchcU_lHggv_ufQ")
.withUndisclosedClaim("region", "G02NSrQfjFXQ7Io09syajA")
.withUndisclosedClaim("country", "lklxF5jMYlGTPUovMNIvCA")
.withDecoyClaim("2GLC42sKQveCfGfryNRN9w")
.withDecoyClaim("eluV5Og3gSNII8EYnsxA_A")
.withDecoyClaim("6Ij7tM-a5iVPGboS5tmvVA")
.withDecoyClaim("eI8ZWm9QnKPpNPeNenHdhQ")
.build();
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("sub", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
.withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA")
.withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ")
.withUndisclosedClaim("phone_number", "Qg_O64zqAxe412a108iroA")
.withUndisclosedClaim("birthdate", "yytVbdAPGcgl2rI4C9GSog")
.withDecoyClaim("AJx-095VPrpTtN4QMOqROA")
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
.build();
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-holder-claims.json");
// Read claims provided by the holder
JsonNode addressClaimSet = holderClaimSet.get("address");
// produce the nested sdJwt
SdJwt addrSdJWT = SdJwt.builder()
.withDisclosureSpec(addrDisclosureSpec)
.withClaimSet(addressClaimSet)
.build();
JsonNode addPayload = addrSdJWT.asNestedPayload();
JsonNode expectedAddrPayload = TestUtils.readClaimSet(getClass(),
"sdjwt/a1.example2-address-payload.json");
assertEquals(expectedAddrPayload, addPayload);
// Verify nested claim has 4 disclosures
assertEquals(4, addrSdJWT.getDisclosures().size());
// Set payload back into main claim set
((ObjectNode) holderClaimSet).set("address", addPayload);
// Read claims added by the issuer & merge both
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-claims.json");
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
// produce the main sdJwt, adding nested sdJwts
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.withNestedSdJwt(addrSdJWT)
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
// Verify all claims are present.
// 10 disclosures from 16 digests (6 decoy claims & decoy array elements)
assertEquals(10, sdJwt.getDisclosures().size());
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import java.io.IOException;
import org.junit.Test;
import org.keycloak.jose.jws.crypto.HashUtils;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtUtilsTest {
/**
* Verify hash production and base 64 url encoding
* Verify algorithm denomination for keycloak encoding.
*/
@Test
public void testHashDisclosure() {
String expected = "uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY";
byte[] hash = HashUtils.hash("SHA-256", "WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0".getBytes());
assertEquals(expected, SdJwtUtils.encodeNoPad(hash));
}
/**
* Verify hash production and base 64 url encoding
* Verify algorithm denomination for keycloak encoding.
*/
@Test
public void testHashDisclosure2() {
String expected = "w0I8EKcdCtUPkGCNUrfwVp2xEgNjtoIDlOxc9-PlOhs";
byte[] hash = HashUtils.hash("SHA-256", "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0".getBytes());
assertEquals(expected, SdJwtUtils.encodeNoPad(hash));
}
/**
* Test the base64 URL encoding of this json string from the spec,
* with whitespace between array elements.
*
* ["_26bc4LT-ac6q2KI6cBW5es", "family_name", "Möbius"]
*
* shall produce
* WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0
*
* There is no padding in the expected string.
*
* see
* https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html#section-5.2.1
*
* @throws IOException
*/
@Test
public void testBase64urlEncodedObjectWhiteSpacedJsonArray() {
String input = "[\"_26bc4LT-ac6q2KI6cBW5es\", \"family_name\", \"Möbius\"]";
// Expected Base64 URL encoded string
String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0";
// Assert that the base64 URL encoded string from the object matches the
// expected string
assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes()));
}
/**
* As we are expexting json serializer to behave differently
*
* https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html#section-5.2.1
*
* @throws IOException
*/
@Test
public void testBase64urlEncodedObjectNoWhiteSpacedJsonArray() {
// Test the base64 URL encoding of this json string from the spec,
// no whitespace between array elements
String input = "[\"_26bc4LT-ac6q2KI6cBW5es\",\"family_name\",\"Möbius\"]";
// Expected Base64 URL encoded string
String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd";
// Assert that the base64 URL encoded string from the object matches the
// expected string
assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes()));
}
@Test
public void testBase64urlEncodedArrayElementWhiteSpacedJsonArray() {
String input = "[\"lklxF5jMYlGTPUovMNIvCA\", \"FR\"]";
// Expected Base64 URL encoded string
String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0";
// Assert that the base64 URL encoded string from the object matches the
// expected string
assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes()));
}
@Test
public void testBase64urlEncodedArrayElementNoWhiteSpacedJsonArray() {
String input = "[\"lklxF5jMYlGTPUovMNIvCA\",\"FR\"]";
// Expected Base64 URL encoded string
String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ";
// Assert that the base64 URL encoded string from the object matches the
// expected string
assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes()));
}
}

View file

@ -0,0 +1,234 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Import test-settings from:
* https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class TestSettings {
public final SignatureSignerContext holderSigContext;
public final SignatureSignerContext issuerSigContext;
public final SignatureVerifierContext holderVerifierContext;
public final SignatureVerifierContext issuerVerifierContext;
private static TestSettings instance = null;
public static TestSettings getInstance() {
if (instance == null) {
instance = new TestSettings();
}
return instance;
}
public SignatureSignerContext getIssuerSignerContext() {
return issuerSigContext;
}
public SignatureSignerContext getHolderSignerContext() {
return holderSigContext;
}
public SignatureVerifierContext getIssuerVerifierContext() {
return issuerVerifierContext;
}
public SignatureVerifierContext getHolderVerifierContext() {
return holderVerifierContext;
}
// private constructor
private TestSettings() {
JsonNode testSettings = TestUtils.readClaimSet(getClass(), "sdjwt/test-settings.json");
JsonNode keySettings = testSettings.get("key_settings");
holderSigContext = initSigContext(keySettings, "holder_key", "ES256", "holder");
issuerSigContext = initSigContext(keySettings, "issuer_key", "ES256", "doc-signer-05-25-2022");
holderVerifierContext = initVerifierContext(keySettings, "holder_key", "ES256", "holder");
issuerVerifierContext = initVerifierContext(keySettings, "issuer_key", "ES256", "doc-signer-05-25-2022");
}
private static SignatureSignerContext initSigContext(JsonNode keySettings, String keyName, String algorithm,
String kid) {
JsonNode keySetting = keySettings.get(keyName);
KeyPair keyPair = readKeyPair(keySetting);
return getSignatureSignerContext(keyPair, algorithm, kid);
}
private static SignatureVerifierContext initVerifierContext(JsonNode keySettings, String keyName, String algorithm,
String kid) {
JsonNode keySetting = keySettings.get(keyName);
KeyPair keyPair = readKeyPair(keySetting);
return getSignatureVerifierContext(keyPair.getPublic(), algorithm, kid);
}
private static KeyPair readKeyPair(JsonNode keySetting) {
String curveName = keySetting.get("crv").asText();
String base64UrlEncodedD = keySetting.get("d").asText();
String base64UrlEncodedX = keySetting.get("x").asText();
String base64UrlEncodedY = keySetting.get("y").asText();
return readEcdsaKeyPair(curveName, base64UrlEncodedD, base64UrlEncodedX, base64UrlEncodedY);
}
public static SignatureVerifierContext verifierContextFrom(JsonNode keyData, String algorithm) {
PublicKey publicKey = readPublicKey(keyData);
return getSignatureVerifierContext(publicKey, algorithm, KeyUtils.createKeyId(publicKey));
}
private static PublicKey readPublicKey(JsonNode keyData) {
if (keyData.has("jwk")) {
keyData = keyData.get("jwk");
}
String curveName = keyData.get("crv").asText();
String base64UrlEncodedX = keyData.get("x").asText();
String base64UrlEncodedY = keyData.get("y").asText();
return readEcdsaPublic(curveName, base64UrlEncodedX, base64UrlEncodedY);
}
private static PublicKey readEcdsaPublic(String curveName, String base64UrlEncodedX,
String base64UrlEncodedY) {
ECParameterSpec ecSpec = getECParameterSpec(ECDSA_CURVE_2_SPECS_NAMES.get(curveName));
byte[] xBytes = Base64Url.decode(base64UrlEncodedX);
byte[] yBytes = Base64Url.decode(base64UrlEncodedY);
try {
KeyFactory keyFactory = KeyFactory.getInstance("EC");
// Generate ECPrivateKey
// Instantiate ECPoint
BigInteger xValue = new BigInteger(1, xBytes);
BigInteger yValue = new BigInteger(1, yBytes);
ECPoint point = new ECPoint(xValue, yValue);
// Generate ECPublicKey
return keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static KeyPair readEcdsaKeyPair(String curveName, String base64UrlEncodedD, String base64UrlEncodedX,
String base64UrlEncodedY) {
ECParameterSpec ecSpec = getECParameterSpec(ECDSA_CURVE_2_SPECS_NAMES.get(curveName));
byte[] dBytes = Base64Url.decode(base64UrlEncodedD);
byte[] xBytes = Base64Url.decode(base64UrlEncodedX);
byte[] yBytes = Base64Url.decode(base64UrlEncodedY);
try {
KeyFactory keyFactory = KeyFactory.getInstance("EC");
// Generate ECPrivateKey
BigInteger dValue = new BigInteger(1, dBytes);
PrivateKey privateKey = keyFactory.generatePrivate(new ECPrivateKeySpec(dValue, ecSpec));
// Instantiate ECPoint
BigInteger xValue = new BigInteger(1, xBytes);
BigInteger yValue = new BigInteger(1, yBytes);
ECPoint point = new ECPoint(xValue, yValue);
// Generate ECPublicKey
PublicKey publicKey = keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec));
return new KeyPair(publicKey, privateKey);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static final Map<String, ECParameterSpec> ECDSA_KEY_SPECS = new HashMap<>();
private static ECParameterSpec getECParameterSpec(String paramSpecName) {
return ECDSA_KEY_SPECS.computeIfAbsent(paramSpecName, TestSettings::generateEcdsaKeySpec);
}
// generate key spec
private static ECParameterSpec generateEcdsaKeySpec(String paramSpecName) {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(paramSpecName);
keyPairGenerator.initialize(ecGenParameterSpec);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
return ((java.security.interfaces.ECPublicKey) keyPair.getPublic()).getParams();
} catch (Exception e) {
throw new RuntimeException("Error obtaining ECParameterSpec for P-256 curve", e);
}
}
private static SignatureSignerContext getSignatureSignerContext(KeyPair keyPair, String algorithm, String kid) {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(algorithm);
keyWrapper.setPrivateKey(keyPair.getPrivate());
keyWrapper.setPublicKey(keyPair.getPublic());
keyWrapper.setType(keyPair.getPublic().getAlgorithm());
keyWrapper.setUse(KeyUse.SIG);
keyWrapper.setKid(kid);
return new AsymmetricSignatureSignerContext(keyWrapper);
}
private static SignatureVerifierContext getSignatureVerifierContext(PublicKey publicKey, String algorithm,
String kid) {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(algorithm);
keyWrapper.setPublicKey(publicKey);
keyWrapper.setType(publicKey.getAlgorithm());
keyWrapper.setUse(KeyUse.SIG);
keyWrapper.setKid(kid);
return new AsymmetricSignatureVerifierContext(keyWrapper);
}
private static final Map<String, String> ECDSA_CURVE_2_SPECS_NAMES = new HashMap<>();
private static final void curveToSpecName() {
ECDSA_CURVE_2_SPECS_NAMES.put("P-256", "secp256r1");
}
static {
curveToSpecName();
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import com.fasterxml.jackson.databind.JsonNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class TestUtils {
public static JsonNode readClaimSet(Class<?> klass, String path) {
// try-with-resources closes inputstream!
try (InputStream is = klass.getClassLoader().getResourceAsStream(path)) {
return SdJwtUtils.mapper.readTree(is);
} catch (IOException e) {
throw new RuntimeException("Error reading file at path: " + path, e);
}
}
public static String readFileAsString(Class<?> klass, String filePath) {
StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
(new InputStreamReader(klass.getClassLoader().getResourceAsStream(filePath))))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line); // Appends line without a newline character
}
} catch (IOException e) {
throw new RuntimeException("Error reading file at path: " + filePath, e);
}
return stringBuilder.toString();
}
public static String splitStringIntoLines(String input, int lineLength) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < input.length(); i += lineLength) {
int end = Math.min(input.length(), i + lineLength);
result.append(input, i, end).append("\n");
}
return result.toString();
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.fasterxml.jackson.databind.node.TextNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class UndisclosedClaimTest {
@Before
public void setUp() throws Exception {
SdJwtUtils.arrayEltSpaced = false;
}
@After
public void tearDown() throws Exception {
SdJwtUtils.arrayEltSpaced = true;
}
@Test
public void testToBase64urlEncoded() {
// Create an instance of UndisclosedClaim with the specified fields
UndisclosedClaim undisclosedClaim = UndisclosedClaim.builder()
.withClaimName("family_name")
.withSalt(new SdJwtSalt("_26bc4LT-ac6q2KI6cBW5es"))
.withClaimValue(new TextNode("Möbius"))
.build();
// Expected Base64 URL encoded string
String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd";
// Assert that the base64 URL encoded string from the object matches the
// expected string
assertEquals(expected, undisclosedClaim.getDisclosureStrings().get(0));
}
}

View file

@ -0,0 +1,196 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt.sdjwtvp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.junit.Test;
import org.keycloak.common.VerificationException;
import org.keycloak.sdjwt.DisclosureSpec;
import org.keycloak.sdjwt.IssuerSignedJWT;
import org.keycloak.sdjwt.SdJwt;
import org.keycloak.sdjwt.TestSettings;
import org.keycloak.sdjwt.TestUtils;
import org.keycloak.sdjwt.vp.SdJwtVP;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class SdJwtVPTest {
// Additional tests can be written to cover edge cases, error conditions,
// and any other functionality specific to the SdJwt class.
@Test
public void testIssuerSignedJWTWithUndiclosedClaims3_3() {
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w")
.withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A")
.withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA")
.withUndisclosedClaim("phone_number", "eI8ZWm9QnKPpNPeNenHdhQ")
.withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA")
.withUndisclosedClaim("birthdate", "AJx-095VPrpTtN4QMOqROA")
.withUndisclosedClaim("is_over_18", "Pc33JM2LchcU_lHggv_ufQ")
.withUndisclosedClaim("is_over_21", "G02NSrQfjFXQ7Io09syajA")
.withUndisclosedClaim("is_over_65", "lklxF5jMYlGTPUovMNIvCA")
.build();
// Read claims provided by the holder
JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json");
// Read claims added by the issuer
JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-claims.json");
// Merge both
((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet);
SdJwt sdJwt = SdJwt.builder()
.withDisclosureSpec(disclosureSpec)
.withClaimSet(holderClaimSet)
.withSigner(TestSettings.getInstance().getIssuerSignerContext())
.build();
IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT();
JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-payload.json");
assertEquals(expected, jwt.getPayload());
String sdJwtString = sdJwt.toSdJwtString();
SdJwtVP actualSdJwt = SdJwtVP.of(sdJwtString);
String expectedString = TestUtils.readFileAsString(getClass(), "sdjwt/s3.3-unsecured-sd-jwt.txt");
SdJwtVP expecteSdJwt = SdJwtVP.of(expectedString);
TestCompareSdJwt.compare(expecteSdJwt, actualSdJwt);
}
@Test
public void testIssuerSignedJWTWithUndiclosedClaims6_1() {
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.1-issued-payload.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
// System.out.println(sdJwtVP.verbose());
assertEquals(0, sdJwtVP.getRecursiveDigests().size());
assertEquals(0, sdJwtVP.getGhostDigests().size());
}
@Test
public void testA1_Example2_with_nested_disclosure_and_decoy_claims() {
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/a1.example2-sdjwt.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
// System.out.println(sdJwtVP.verbose());
assertEquals(10, sdJwtVP.getDisclosures().size());
assertEquals(0, sdJwtVP.getRecursiveDigests().size());
assertEquals(0, sdJwtVP.getGhostDigests().size());
}
@Test
public void testS7_3_RecursiveDisclosureOfStructuredSdJwt() {
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
// System.out.println(sdJwtVP.verbose());
assertEquals(5, sdJwtVP.getDisclosures().size());
assertEquals(4, sdJwtVP.getRecursiveDigests().size());
assertEquals(0, sdJwtVP.getGhostDigests().size());
}
@Test
public void testS7_3_GhostDisclosures() {
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt+ghost.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
// System.out.println(sdJwtVP.verbose());
assertEquals(8, sdJwtVP.getDisclosures().size());
assertEquals(4, sdJwtVP.getRecursiveDigests().size());
assertEquals(3, sdJwtVP.getGhostDigests().size());
}
@Test
public void testS7_3_VerifyIssuerSignaturePositive() throws VerificationException {
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
sdJwtVP.getIssuerSignedJWT().verifySignature(TestSettings.getInstance().getIssuerVerifierContext());
}
@Test(expected = VerificationException.class)
public void testS7_3_VerifyIssuerSignatureNegative() throws VerificationException {
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
sdJwtVP.getIssuerSignedJWT().verifySignature(TestSettings.getInstance().getHolderVerifierContext());
}
@Test
public void testS6_2_PresentationPositive() throws VerificationException {
String jwsType = "vc+sd-jwt";
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json");
String presentation = sdJwtVP.present(null, keyBindingClaims,
TestSettings.getInstance().getHolderSignerContext(), jwsType);
SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation);
assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent());
// Verify with public key from settings
presenteSdJwtVP.getKeyBindingJWT().get().verifySignature(TestSettings.getInstance().getHolderVerifierContext());
// Verify with public key from cnf claim
presenteSdJwtVP.getKeyBindingJWT().get()
.verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256"));
}
@Test(expected = VerificationException.class)
public void testS6_2_PresentationNegative() throws VerificationException {
String jwsType = "vc+sd-jwt";
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json");
String presentation = sdJwtVP.present(null, keyBindingClaims,
TestSettings.getInstance().getHolderSignerContext(), jwsType);
SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation);
assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent());
// Verify with public key from cnf claim
presenteSdJwtVP.getKeyBindingJWT().get()
.verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256"));
// Verify with wrong public key from settings (iisuer)
presenteSdJwtVP.getKeyBindingJWT().get().verifySignature(TestSettings.getInstance().getIssuerVerifierContext());
}
@Test
public void testS6_2_PresentationPartialDisclosure() throws VerificationException {
String jwsType = "vc+sd-jwt";
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json");
// disclose only the given_name
String presentation = sdJwtVP.present(Arrays.asList("jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"),
keyBindingClaims, TestSettings.getInstance().getHolderSignerContext(), jwsType);
SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation);
assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent());
// Verify with public key from cnf claim
presenteSdJwtVP.getKeyBindingJWT().get()
.verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256"));
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt.sdjwtvp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.common.util.Base64Url;
import org.keycloak.sdjwt.IssuerSignedJWT;
import org.keycloak.sdjwt.SdJwtUtils;
import org.keycloak.sdjwt.vp.SdJwtVP;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
/**
* This class will try to test conformity to the spec by comparing json objects.
*
*
* We are facing the situation that:
* - json produced are not normalized. But we can compare them by natching their
* content once loaded into a json object.
* - ecdsa signature contains random component. We can't compare them directly.
* Even if we had the same input byte
* - The no rationale for ordering the disclosures. So we can only make sure
* each of them is present and that the json content matches.
*
* Warning: in orther to produce the same disclosure strings and hashes like in
* the spect, i had to produce
* the same print. This is by no way reliable enougth to be used to test
* conformity to the spec.
*
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class TestCompareSdJwt {
public static void compare(SdJwtVP expectedSdJwt, SdJwtVP actualSdJwt) {
try {
compareIssuerSignedJWT(expectedSdJwt.getIssuerSignedJWT(), actualSdJwt.getIssuerSignedJWT());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
compareDisclosures(expectedSdJwt, actualSdJwt);
}
private static void compareIssuerSignedJWT(IssuerSignedJWT e, IssuerSignedJWT a)
throws JsonMappingException, JsonProcessingException {
assertEquals(e.getPayload(), a.getPayload());
List<String> expectedJwsStrings = Arrays.asList(e.getJwsString().split("\\."));
List<String> actualJwsStrings = Arrays.asList(a.getJwsString().split("\\."));
// compare json content of header
assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0)));
// compare payload
assertEquals(toJsonNode(expectedJwsStrings.get(1)), toJsonNode(actualJwsStrings.get(1)));
// We wont compare signatures.
}
private static void compareDisclosures(SdJwtVP expectedSdJwt, SdJwtVP actualSdJwt) {
Set<JsonNode> expectedDisclosures = expectedSdJwt.getDisclosuresString().stream()
.map(TestCompareSdJwt::toJsonNode)
.collect(Collectors.toSet());
Set<JsonNode> actualDisclosures = expectedSdJwt.getDisclosuresString().stream()
.map(TestCompareSdJwt::toJsonNode)
.collect(Collectors.toSet());
assertEquals(expectedDisclosures.size(), actualDisclosures.size());
boolean foundEqualPair = false;
for (JsonNode a : expectedDisclosures) {
for (JsonNode b : actualDisclosures) {
if (a.equals(b)) {
foundEqualPair = true;
break;
}
}
}
assertTrue("The set should contain equal elements", foundEqualPair);
}
private static JsonNode toJsonNode(String base64EncodedString) {
try {
return SdJwtUtils.mapper.readTree(Base64Url.decode(base64EncodedString));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,12 @@
{
"_sd": [
"IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8",
"Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo",
"QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM",
"UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg",
"cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo",
"oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA",
"qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74",
"uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4"
]
}

View file

@ -0,0 +1,14 @@
{
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"given_name": "太郎",
"family_name": "山田",
"email": "\"unusual email address\"@example.jp",
"phone_number": "+81-80-1234-5678",
"address": {
"street_address": "東京都港区芝公園4丁目2−8",
"locality": "東京都",
"region": "港区",
"country": "JP"
},
"birthdate": "1940-01-01"
}

View file

@ -0,0 +1,5 @@
{
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000
}

View file

@ -0,0 +1,28 @@
{
"_sd": [
"9hf5niUdeWrPmaU5mz727OELoKHX5TDZjrBVHCVzqcg",
"Kfv8UXTNDG2NWPv6CtT5QAa-w5-ugOfICaoap474crk",
"Kuet1yAa0HIQvYnOVd59hcViO9Ug6J2kSfqYRBeowvE",
"MMldOFFzB2d0umlmpTIaGerhWdU_PpYfLvKhh_f_9aY",
"X6ZAYOII2vPN40V7xExZwVwz7yRmLNcVwt5DL8RLv4g",
"ihDxP1pJ59-iRb-aft25j3cqC1ShChhO_sWC02gVUGw",
"s0BKYsLWxQQeU8tVlltM7MKsIRTrEIa1PkJmqxBBf5U",
"vg70gfzXO8HR7ERDkL46S6Ior1ey0DvZoEUHupJwoxc"
],
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000,
"address": {
"_sd": [
"IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8",
"Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo",
"QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM",
"UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg",
"cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo",
"oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA",
"qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74",
"uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4"
]
},
"_sd_alg": "sha-256"
}

View file

@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiOWhmNW5pVWRlV3JQbWFVNW16NzI3T0VMb0tIWDVURFpqckJWSENWenFjZyIsIktmdjhVWFROREcyTldQdjZDdFQ1UUFhLXc1LXVnT2ZJQ2FvYXA0NzRjcmsiLCJLdWV0MXlBYTBISVF2WW5PVmQ1OWhjVmlPOVVnNkoya1NmcVlSQmVvd3ZFIiwiTU1sZE9GRnpCMmQwdW1sbXBUSWFHZXJoV2RVX1BwWWZMdktoaF9mXzlhWSIsIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCJpaER4UDFwSjU5LWlSYi1hZnQyNWozY3FDMVNoQ2hoT19zV0MwMmdWVUd3IiwiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSIsInZnNzBnZnpYTzhIUjdFUkRrTDQ2UzZJb3IxZXkwRHZab0VVSHVwSndveGMiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJJWk95Sm4wRDE3YUs4NDVpWjhpNWhEbG9Ta2FydGlVbHZwX2hLak5iQXQ4IiwiTHN4X3R3LVV3RVpfSHNLOFFjSURrQ241V3ZmZTVCbXZjbldRQjZpa3FxbyIsIlFOUWQzX3k4bzZxdGRKZ3VRRHVBM3pkZmRZei1XZ0xTYWplMDZzMlVtV00iLCJVV3p2Q0JVUll4NGRTa2VCQ3B0Z0x0dWRGYkxnbkpvQmdtYUhCLTc2bE9nIiwiY3g0dG9FYjFxQVJrQWY4TnVEMEFUazNvTTZtOGEwcThuQVZGRHRCZGZvbyIsIm9VdUU5MERVQ3gzWHVfSDV6UU1CRXFBZGJNcmxBWjdRb0s1eklKX0IxbUEiLCJxZDJHNVRHSC02TTFxTnk4b3VoWGZzRTdVNnZXRE9ucDJGVXZvdkFhVzc0IiwidU5Ib1dZaFhzWmhWSkNORTJEcXktenF0N3Q2OWdKS3k1UWFGdjdHck1YNCJdfSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMH0.MEQCIDiR0hG5E-jCC6YEr1nrTJSOwIn7FL8FmQWhJfFgkjRrAiAPPnEfgnBRiad2RyfNjIx6UzGV2TP0SYLhNTm6syGMjw~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAi5aSq6YOOIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIuWxseeUsCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0~WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~

View file

@ -0,0 +1,17 @@
{
"vct": "https://credentials.example.com/identity_credential",
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"is_over_18": true,
"is_over_21": true,
"is_over_65": true
}

View file

@ -0,0 +1,14 @@
{
"iss": "https://example.com/issuer",
"iat": 1683000000,
"exp": 1883000000,
"vct": "https://credentials.example.com/identity_credential",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
}
}
}

View file

@ -0,0 +1,26 @@
{
"_sd": [
"09vKrJMOlyTWM0sjpu_pdOBVBQ2M1y3KhpH515nXkpY",
"2rsjGbaC0ky8mT0pJrPioWTq0_daw1sX76poUlgCwbI",
"EkO8dhW0dHEJbvUHlE_VCeuC9uRELOieLZhh7XbUTtA",
"IlDzIKeiZdDwpqpK6ZfbyphFvz5FgnWa-sN6wqQXCiw",
"JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE",
"PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI",
"TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo",
"jdrTE8YcbY4EifugihiAe_BPekxJQZICeiUQwY9QqxI",
"jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"
],
"iss": "https://example.com/issuer",
"iat": 1683000000,
"exp": 1883000000,
"vct": "https://credentials.example.com/identity_credential",
"_sd_alg": "sha-256",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
}
}
}

View file

@ -0,0 +1,28 @@
eyJhbGciOiAiRVMyNTYiLCAia2lkIjogImRvYy1zaWduZXItMDUtMjUtMjAyMiIsICJ0
eXAiOiAidmMrc2Qtand0In0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9C
VkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9k
YXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9p
ZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNO
NndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQ
WWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJ
IiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAi
amRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5
eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAi
aHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4
cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxl
LmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJj
bmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRD
QUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJa
eGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.YHjaS
waBy-6hBYBre1F1ehiHNp69F9jnP2Hve3g0gNTzG_6GxV-E9rPR5m_CCo1SgDk0GaE5S
II6FBprkwDP-Q~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLC
AiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgI
kRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VA
ZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251b
WJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIi
wgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2
FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOi
AiVVMifV0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOT
QwLTAxLTAxIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLC
B0cnVlXQ~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnV
lXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~

View file

@ -0,0 +1,20 @@
{
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"email": "johndoe@example.com",
"phone_number": "+1-202-555-0101",
"phone_number_verified": true,
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"birthdate": "1940-01-01",
"updated_at": 1570000000,
"nationalities": [
"US",
"DE"
]
}

View file

@ -0,0 +1,29 @@
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb
IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ
akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL
dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1
SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB
TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2
Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr
b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn
bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu
Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog
InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15
VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1
ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog
InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y
NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH
ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG
MkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BK
wIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgI
mdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZh
bWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWl
sIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhR
IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4Z
TQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngt
MDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjog
IjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFu
eXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZR
IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5
YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92T
U5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~

View file

@ -0,0 +1,19 @@
{
"_sd": [
"cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI",
"dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88",
"fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM",
"sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM"
],
"_sd_alg": "sha-256",
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"phone_number_verified": true,
"updated_at": 1570000000,
"nationalities": [
{ "...": "mXYRA4kcMm9hHUX-dCc44jKpyrNiEtJo2IqLk5YzRik" },
{ "...": "XkluhXNRk-Gmh8zBHo4Ad3drmukEbmm4CECMCefdG24" },
{ "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" }
]
}

View file

@ -0,0 +1,18 @@
{
"_sd": [
"cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI",
"dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88",
"fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM",
"sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM"
],
"_sd_alg": "sha-256",
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"phone_number_verified": true,
"updated_at": 1570000000,
"nationalities": [
"US",
{ "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" }
]
}

View file

@ -0,0 +1,18 @@
{
"_sd": [
"cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI",
"dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88",
"fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM",
"sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM"
],
"_sd_alg": "sha-256",
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"phone_number_verified": true,
"updated_at": 1570000000,
"nationalities": [
"US",
"DE"
]
}

View file

@ -0,0 +1,5 @@
{
"nonce": "1234567890",
"aud": "https://verifier.example.org",
"iat": 1702315679
}

View file

@ -0,0 +1,23 @@
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb
IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ
akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL
dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1
SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB
TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2
Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr
b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn
bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu
Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog
InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15
VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1
ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog
InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y
NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH
ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG
MkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BK
wIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgI
mZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFk
ZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5
IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMi
fV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd
~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~

View file

@ -0,0 +1,9 @@
{
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"street_address": "Schulstr. 12",
"locality": "Schulpforta",
"region": "Sachsen-Anhalt",
"country": "DE"
}
}

View file

@ -0,0 +1,5 @@
{
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000
}

View file

@ -0,0 +1,8 @@
{
"_sd": ["fOBUSQvo46yQO-wRwXBcGqvnbKIueISEL961_Sjd4do"],
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000,
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"_sd_alg": "sha-256"
}

View file

@ -0,0 +1,15 @@
{
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000,
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"_sd": [
"6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0",
"9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM",
"KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88",
"WN9r9dCBJ8HTCsS2jKASxTjEyW5m5x65_Z_2ro2jfXM"
]
},
"_sd_alg": "sha-256"
}

View file

@ -0,0 +1,15 @@
{
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000,
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"address": {
"_sd": [
"6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0",
"9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM",
"KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88"
],
"country": "DE"
},
"_sd_alg": "sha-256"
}

View file

@ -0,0 +1,8 @@
{
"_sd": ["HvrKX6fPV0v9K_yCVFBiLFHsMaxcD_114Em6VT8x1lg"],
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000,
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
"_sd_alg": "sha-256"
}

View file

@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~

View file

@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~

View file

@ -0,0 +1,29 @@
{
"identifiers": {
"issuer": "https://example.com/issuer",
"verifier": "https://example.com/verifier"
},
"key_settings": {
"key_size": 256,
"kty": "EC",
"issuer_key": {
"kty": "EC",
"d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g",
"crv": "P-256",
"x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ",
"y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8"
},
"holder_key": {
"kty": "EC",
"d": "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
}
},
"key_binding_nonce": "1234567890",
"expiry_seconds": 86400000,
"random_seed": 0,
"iat": 1683000000,
"exp": 1883000000
}

View file

@ -0,0 +1,32 @@
# from: https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml
identifiers:
issuer: "https://example.com/issuer"
verifier: "https://example.com/verifier"
key_settings:
key_size: 256
kty: EC
issuer_key:
kty: EC
d: Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g
crv: P-256
x: b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ
y: Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8
holder_key:
kty: EC
d: 5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I
crv: P-256
x: TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc
y: ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ
key_binding_nonce: "1234567890"
expiry_seconds: 86400000 # 1000 days
random_seed: 0
iat: 1683000000 # Tue May 02 2023 04:00:00 GMT+0000
exp: 1883000000 # Sat Sep 01 2029 23:33:20 GMT+0000