OID4VC: Keycloak native support of SD-JWT (#25829)
Closes #25638 Signed-off-by: Francis Pouatcha <francis.pouatcha@adorsys.com>
This commit is contained in:
parent
aa6b102e3d
commit
f7e60b4338
63 changed files with 3981 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -65,6 +65,7 @@ nbproject
|
|||
# Maven #
|
||||
#########
|
||||
target
|
||||
bin
|
||||
|
||||
# Maven shade
|
||||
#############
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
126
core/src/main/java/org/keycloak/sdjwt/ArrayDisclosure.java
Normal file
126
core/src/main/java/org/keycloak/sdjwt/ArrayDisclosure.java
Normal 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();
|
||||
}
|
||||
}
|
65
core/src/main/java/org/keycloak/sdjwt/DecoyArrayElement.java
Normal file
65
core/src/main/java/org/keycloak/sdjwt/DecoyArrayElement.java
Normal 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();
|
||||
}
|
||||
}
|
46
core/src/main/java/org/keycloak/sdjwt/DecoyClaim.java
Normal file
46
core/src/main/java/org/keycloak/sdjwt/DecoyClaim.java
Normal 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();
|
||||
}
|
||||
}
|
43
core/src/main/java/org/keycloak/sdjwt/DecoyEntry.java
Normal file
43
core/src/main/java/org/keycloak/sdjwt/DecoyEntry.java
Normal 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()));
|
||||
}
|
||||
}
|
77
core/src/main/java/org/keycloak/sdjwt/Disclosable.java
Normal file
77
core/src/main/java/org/keycloak/sdjwt/Disclosable.java
Normal 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();
|
||||
}
|
||||
}
|
53
core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java
Normal file
53
core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java
Normal 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);
|
||||
}
|
||||
}
|
191
core/src/main/java/org/keycloak/sdjwt/DisclosureSpec.java
Normal file
191
core/src/main/java/org/keycloak/sdjwt/DisclosureSpec.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
199
core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java
Normal file
199
core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
110
core/src/main/java/org/keycloak/sdjwt/SdJws.java
Normal file
110
core/src/main/java/org/keycloak/sdjwt/SdJws.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
235
core/src/main/java/org/keycloak/sdjwt/SdJwt.java
Normal file
235
core/src/main/java/org/keycloak/sdjwt/SdJwt.java
Normal 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();
|
||||
}
|
||||
}
|
34
core/src/main/java/org/keycloak/sdjwt/SdJwtArrayElement.java
Normal file
34
core/src/main/java/org/keycloak/sdjwt/SdJwtArrayElement.java
Normal 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();
|
||||
}
|
38
core/src/main/java/org/keycloak/sdjwt/SdJwtClaim.java
Normal file
38
core/src/main/java/org/keycloak/sdjwt/SdJwtClaim.java
Normal 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();
|
||||
|
||||
}
|
54
core/src/main/java/org/keycloak/sdjwt/SdJwtClaimName.java
Normal file
54
core/src/main/java/org/keycloak/sdjwt/SdJwtClaimName.java
Normal 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;
|
||||
}
|
||||
}
|
47
core/src/main/java/org/keycloak/sdjwt/SdJwtSalt.java
Normal file
47
core/src/main/java/org/keycloak/sdjwt/SdJwtSalt.java
Normal 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);
|
||||
}
|
||||
}
|
107
core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java
Normal file
107
core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java
Normal 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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
98
core/src/main/java/org/keycloak/sdjwt/UndisclosedClaim.java
Normal file
98
core/src/main/java/org/keycloak/sdjwt/UndisclosedClaim.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
73
core/src/main/java/org/keycloak/sdjwt/VisibleSdJwtClaim.java
Normal file
73
core/src/main/java/org/keycloak/sdjwt/VisibleSdJwtClaim.java
Normal 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();
|
||||
}
|
||||
}
|
48
core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java
Normal file
48
core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java
Normal 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);
|
||||
}
|
||||
}
|
265
core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java
Normal file
265
core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
133
core/src/test/java/org/keycloak/sdjwt/IssuerSignedJWTTest.java
Normal file
133
core/src/test/java/org/keycloak/sdjwt/IssuerSignedJWTTest.java
Normal 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());
|
||||
}
|
||||
}
|
78
core/src/test/java/org/keycloak/sdjwt/JsonClaimsetTest.java
Normal file
78
core/src/test/java/org/keycloak/sdjwt/JsonClaimsetTest.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
175
core/src/test/java/org/keycloak/sdjwt/SdJWTSamplesTest.java
Normal file
175
core/src/test/java/org/keycloak/sdjwt/SdJWTSamplesTest.java
Normal 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());
|
||||
}
|
||||
}
|
105
core/src/test/java/org/keycloak/sdjwt/SdJwtTest.java
Normal file
105
core/src/test/java/org/keycloak/sdjwt/SdJwtTest.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
124
core/src/test/java/org/keycloak/sdjwt/SdJwtUtilsTest.java
Normal file
124
core/src/test/java/org/keycloak/sdjwt/SdJwtUtilsTest.java
Normal 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()));
|
||||
}
|
||||
}
|
234
core/src/test/java/org/keycloak/sdjwt/TestSettings.java
Normal file
234
core/src/test/java/org/keycloak/sdjwt/TestSettings.java
Normal 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();
|
||||
}
|
||||
}
|
62
core/src/test/java/org/keycloak/sdjwt/TestUtils.java
Normal file
62
core/src/test/java/org/keycloak/sdjwt/TestUtils.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
196
core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java
Normal file
196
core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPTest.java
Normal 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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"_sd": [
|
||||
"IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8",
|
||||
"Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo",
|
||||
"QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM",
|
||||
"UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg",
|
||||
"cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo",
|
||||
"oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA",
|
||||
"qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74",
|
||||
"uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4"
|
||||
]
|
||||
}
|
14
core/src/test/resources/sdjwt/a1.example2-holder-claims.json
Normal file
14
core/src/test/resources/sdjwt/a1.example2-holder-claims.json
Normal 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"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"iss": "https://issuer.example.com",
|
||||
"iat": 1683000000,
|
||||
"exp": 1883000000
|
||||
}
|
|
@ -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"
|
||||
}
|
1
core/src/test/resources/sdjwt/a1.example2-sdjwt.txt
Normal file
1
core/src/test/resources/sdjwt/a1.example2-sdjwt.txt
Normal file
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiOWhmNW5pVWRlV3JQbWFVNW16NzI3T0VMb0tIWDVURFpqckJWSENWenFjZyIsIktmdjhVWFROREcyTldQdjZDdFQ1UUFhLXc1LXVnT2ZJQ2FvYXA0NzRjcmsiLCJLdWV0MXlBYTBISVF2WW5PVmQ1OWhjVmlPOVVnNkoya1NmcVlSQmVvd3ZFIiwiTU1sZE9GRnpCMmQwdW1sbXBUSWFHZXJoV2RVX1BwWWZMdktoaF9mXzlhWSIsIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCJpaER4UDFwSjU5LWlSYi1hZnQyNWozY3FDMVNoQ2hoT19zV0MwMmdWVUd3IiwiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSIsInZnNzBnZnpYTzhIUjdFUkRrTDQ2UzZJb3IxZXkwRHZab0VVSHVwSndveGMiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJJWk95Sm4wRDE3YUs4NDVpWjhpNWhEbG9Ta2FydGlVbHZwX2hLak5iQXQ4IiwiTHN4X3R3LVV3RVpfSHNLOFFjSURrQ241V3ZmZTVCbXZjbldRQjZpa3FxbyIsIlFOUWQzX3k4bzZxdGRKZ3VRRHVBM3pkZmRZei1XZ0xTYWplMDZzMlVtV00iLCJVV3p2Q0JVUll4NGRTa2VCQ3B0Z0x0dWRGYkxnbkpvQmdtYUhCLTc2bE9nIiwiY3g0dG9FYjFxQVJrQWY4TnVEMEFUazNvTTZtOGEwcThuQVZGRHRCZGZvbyIsIm9VdUU5MERVQ3gzWHVfSDV6UU1CRXFBZGJNcmxBWjdRb0s1eklKX0IxbUEiLCJxZDJHNVRHSC02TTFxTnk4b3VoWGZzRTdVNnZXRE9ucDJGVXZvdkFhVzc0IiwidU5Ib1dZaFhzWmhWSkNORTJEcXktenF0N3Q2OWdKS3k1UWFGdjdHck1YNCJdfSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMH0.MEQCIDiR0hG5E-jCC6YEr1nrTJSOwIn7FL8FmQWhJfFgkjRrAiAPPnEfgnBRiad2RyfNjIx6UzGV2TP0SYLhNTm6syGMjw~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAi5aSq6YOOIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIuWxseeUsCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0~WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~
|
17
core/src/test/resources/sdjwt/s3.3-holder-claims.json
Normal file
17
core/src/test/resources/sdjwt/s3.3-holder-claims.json
Normal 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
|
||||
}
|
14
core/src/test/resources/sdjwt/s3.3-issuer-claims.json
Normal file
14
core/src/test/resources/sdjwt/s3.3-issuer-claims.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
26
core/src/test/resources/sdjwt/s3.3-issuer-payload.json
Normal file
26
core/src/test/resources/sdjwt/s3.3-issuer-payload.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
28
core/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt
Normal file
28
core/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt
Normal 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~
|
20
core/src/test/resources/sdjwt/s6.1-holder-claims.json
Normal file
20
core/src/test/resources/sdjwt/s6.1-holder-claims.json
Normal 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"
|
||||
]
|
||||
}
|
29
core/src/test/resources/sdjwt/s6.1-issued-payload.txt
Normal file
29
core/src/test/resources/sdjwt/s6.1-issued-payload.txt
Normal 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~
|
|
@ -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" }
|
||||
]
|
||||
}
|
|
@ -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" }
|
||||
]
|
||||
}
|
18
core/src/test/resources/sdjwt/s6.1-issuer-payload.json
Normal file
18
core/src/test/resources/sdjwt/s6.1-issuer-payload.json
Normal 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"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"nonce": "1234567890",
|
||||
"aud": "https://verifier.example.org",
|
||||
"iat": 1702315679
|
||||
}
|
23
core/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt
Normal file
23
core/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt
Normal 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~
|
9
core/src/test/resources/sdjwt/s7-holder-claims.json
Normal file
9
core/src/test/resources/sdjwt/s7-holder-claims.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
|
||||
"address": {
|
||||
"street_address": "Schulstr. 12",
|
||||
"locality": "Schulpforta",
|
||||
"region": "Sachsen-Anhalt",
|
||||
"country": "DE"
|
||||
}
|
||||
}
|
5
core/src/test/resources/sdjwt/s7-issuer-claims.json
Normal file
5
core/src/test/resources/sdjwt/s7-issuer-claims.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"iss": "https://issuer.example.com",
|
||||
"iat": 1683000000,
|
||||
"exp": 1883000000
|
||||
}
|
8
core/src/test/resources/sdjwt/s7.1-issuer-payload.json
Normal file
8
core/src/test/resources/sdjwt/s7.1-issuer-payload.json
Normal 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"
|
||||
}
|
15
core/src/test/resources/sdjwt/s7.2-issuer-payload.json
Normal file
15
core/src/test/resources/sdjwt/s7.2-issuer-payload.json
Normal 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"
|
||||
}
|
15
core/src/test/resources/sdjwt/s7.2b-issuer-payload.json
Normal file
15
core/src/test/resources/sdjwt/s7.2b-issuer-payload.json
Normal 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"
|
||||
}
|
8
core/src/test/resources/sdjwt/s7.3-issuer-payload.json
Normal file
8
core/src/test/resources/sdjwt/s7.3-issuer-payload.json
Normal 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"
|
||||
}
|
1
core/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt
Normal file
1
core/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt
Normal file
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~
|
1
core/src/test/resources/sdjwt/s7.3-sdjwt.txt
Normal file
1
core/src/test/resources/sdjwt/s7.3-sdjwt.txt
Normal file
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~
|
29
core/src/test/resources/sdjwt/test-settings.json
Normal file
29
core/src/test/resources/sdjwt/test-settings.json
Normal 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
|
||||
}
|
32
core/src/test/resources/sdjwt/test-settings.yml
Normal file
32
core/src/test/resources/sdjwt/test-settings.yml
Normal 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
|
Loading…
Reference in a new issue