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 #
|
# Maven #
|
||||||
#########
|
#########
|
||||||
target
|
target
|
||||||
|
bin
|
||||||
|
|
||||||
# Maven shade
|
# 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