Implement advanced verification of SD-JWT in Keycloak (#30966)
closes #30907 Signed-off-by: Ingrid Kamga <Ingrid.Kamga@adorsys.com>
This commit is contained in:
parent
4080ee2e84
commit
36a141007e
44 changed files with 2620 additions and 44 deletions
|
@ -17,7 +17,10 @@
|
||||||
package org.keycloak.crypto;
|
package org.keycloak.crypto;
|
||||||
|
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.common.crypto.CryptoIntegration;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
|
|
||||||
|
@ -42,7 +45,7 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
|
||||||
@Override
|
@Override
|
||||||
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
|
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
|
||||||
try {
|
try {
|
||||||
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
|
Signature verifier = getSignature();
|
||||||
verifier.initVerify((PublicKey) key.getPublicKey());
|
verifier.initVerify((PublicKey) key.getPublicKey());
|
||||||
verifier.update(data);
|
verifier.update(data);
|
||||||
return verifier.verify(signature);
|
return verifier.verify(signature);
|
||||||
|
@ -51,4 +54,13 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Signature getSignature()
|
||||||
|
throws NoSuchAlgorithmException, NoSuchProviderException {
|
||||||
|
try {
|
||||||
|
return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Retry using the current crypto provider's override implementation
|
||||||
|
return CryptoIntegration.getProvider().getSignature(key.getAlgorithmOrDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ public class DisclosureRedList {
|
||||||
private final Set<SdJwtClaimName> redListClaimNames;
|
private final Set<SdJwtClaimName> redListClaimNames;
|
||||||
public static final DisclosureRedList defaultList = defaultList();
|
public static final DisclosureRedList defaultList = defaultList();
|
||||||
|
|
||||||
public DisclosureRedList of(Set<SdJwtClaimName> redListClaimNames) {
|
public static DisclosureRedList of(Set<SdJwtClaimName> redListClaimNames) {
|
||||||
return new DisclosureRedList(redListClaimNames);
|
return new DisclosureRedList(redListClaimNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,11 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
|
||||||
|
@ -39,13 +42,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
*/
|
*/
|
||||||
public class IssuerSignedJWT extends SdJws {
|
public class IssuerSignedJWT extends SdJws {
|
||||||
|
|
||||||
public static IssuerSignedJWT fromJws(String jwsString) {
|
public IssuerSignedJWT(JsonNode payload, SignatureSignerContext signer, String jwsType) {
|
||||||
return new IssuerSignedJWT(jwsString);
|
super(payload, signer, jwsType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IssuerSignedJWT toSignedJWT(SignatureSignerContext signer, String jwsType) {
|
public static IssuerSignedJWT fromJws(String jwsString) {
|
||||||
JWSInput jwsInput = sign(getPayload(), signer, jwsType);
|
return new IssuerSignedJWT(jwsString);
|
||||||
return new IssuerSignedJWT(getPayload(), jwsInput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IssuerSignedJWT(String jwsString) {
|
private IssuerSignedJWT(String jwsString) {
|
||||||
|
@ -134,6 +136,43 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `cnf` claim (establishing key binding)
|
||||||
|
*/
|
||||||
|
public Optional<JsonNode> getCnfClaim() {
|
||||||
|
var cnf = getPayload().get("cnf");
|
||||||
|
return Optional.ofNullable(cnf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns declared hash algorithm from SD hash claim.
|
||||||
|
*/
|
||||||
|
public String getSdHashAlg() {
|
||||||
|
var hashAlgNode = getPayload().get(CLAIM_NAME_SD_HASH_ALGORITHM);
|
||||||
|
return hashAlgNode == null ? "sha-256" : hashAlgNode.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the SD hash algorithm is understood and deemed secure.
|
||||||
|
*
|
||||||
|
* @throws VerificationException if not
|
||||||
|
*/
|
||||||
|
public void verifySdHashAlgorithm() throws VerificationException {
|
||||||
|
// Known secure algorithms
|
||||||
|
final Set<String> secureAlgorithms = Set.of(
|
||||||
|
"sha-256", "sha-384", "sha-512",
|
||||||
|
"sha3-256", "sha3-384", "sha3-512"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read SD hash claim
|
||||||
|
String hashAlg = getSdHashAlg();
|
||||||
|
|
||||||
|
// Safeguard algorithm
|
||||||
|
if (!secureAlgorithms.contains(hashAlg)) {
|
||||||
|
throw new VerificationException("Unexpected or insecure hash algorithm: " + hashAlg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SD-JWT Claims
|
// SD-JWT Claims
|
||||||
public static final String CLAIM_NAME_SELECTIVE_DISCLOSURE = "_sd";
|
public static final String CLAIM_NAME_SELECTIVE_DISCLOSURE = "_sd";
|
||||||
public static final String CLAIM_NAME_SD_HASH_ALGORITHM = "_sd_alg";
|
public static final String CLAIM_NAME_SD_HASH_ALGORITHM = "_sd_alg";
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for Issuer-signed JWT verification.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class IssuerSignedJwtVerificationOpts {
|
||||||
|
private final SignatureVerifierContext verifier;
|
||||||
|
private final boolean validateIssuedAtClaim;
|
||||||
|
private final boolean validateExpirationClaim;
|
||||||
|
private final boolean validateNotBeforeClaim;
|
||||||
|
|
||||||
|
public IssuerSignedJwtVerificationOpts(
|
||||||
|
SignatureVerifierContext verifier,
|
||||||
|
boolean validateIssuedAtClaim,
|
||||||
|
boolean validateExpirationClaim,
|
||||||
|
boolean validateNotBeforeClaim) {
|
||||||
|
this.verifier = verifier;
|
||||||
|
this.validateIssuedAtClaim = validateIssuedAtClaim;
|
||||||
|
this.validateExpirationClaim = validateExpirationClaim;
|
||||||
|
this.validateNotBeforeClaim = validateNotBeforeClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignatureVerifierContext getVerifier() {
|
||||||
|
return verifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mustValidateIssuedAtClaim() {
|
||||||
|
return validateIssuedAtClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mustValidateExpirationClaim() {
|
||||||
|
return validateExpirationClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mustValidateNotBeforeClaim() {
|
||||||
|
return validateNotBeforeClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IssuerSignedJwtVerificationOpts.Builder builder() {
|
||||||
|
return new IssuerSignedJwtVerificationOpts.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private SignatureVerifierContext verifier;
|
||||||
|
private boolean validateIssuedAtClaim;
|
||||||
|
private boolean validateExpirationClaim = true;
|
||||||
|
private boolean validateNotBeforeClaim = true;
|
||||||
|
|
||||||
|
public Builder withVerifier(SignatureVerifierContext verifier) {
|
||||||
|
this.verifier = verifier;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withValidateIssuedAtClaim(boolean validateIssuedAtClaim) {
|
||||||
|
this.validateIssuedAtClaim = validateIssuedAtClaim;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withValidateExpirationClaim(boolean validateExpirationClaim) {
|
||||||
|
this.validateExpirationClaim = validateExpirationClaim;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withValidateNotBeforeClaim(boolean validateNotBeforeClaim) {
|
||||||
|
this.validateNotBeforeClaim = validateNotBeforeClaim;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IssuerSignedJwtVerificationOpts build() {
|
||||||
|
return new IssuerSignedJwtVerificationOpts(
|
||||||
|
verifier,
|
||||||
|
validateIssuedAtClaim,
|
||||||
|
validateExpirationClaim,
|
||||||
|
validateNotBeforeClaim
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,12 +17,15 @@
|
||||||
package org.keycloak.sdjwt;
|
package org.keycloak.sdjwt;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.crypto.SignatureVerifierContext;
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
import org.keycloak.jose.jws.JWSBuilder;
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
|
import org.keycloak.jose.jws.JWSHeader;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.jose.jws.JWSInputException;
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
|
|
||||||
|
@ -30,11 +33,11 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle jws, either the issuer jwt or the holder key binding jwt.
|
* Handle jws, either the issuer jwt or the holder key binding jwt.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class SdJws {
|
public abstract class SdJws {
|
||||||
private final JWSInput jwsInput;
|
private final JWSInput jwsInput;
|
||||||
private final JsonNode payload;
|
private final JsonNode payload;
|
||||||
|
|
||||||
|
@ -49,10 +52,6 @@ public class SdJws {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getJwsString() {
|
|
||||||
return jwsInput.getWireString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructor for unsigned JWS
|
// Constructor for unsigned JWS
|
||||||
protected SdJws(JsonNode payload) {
|
protected SdJws(JsonNode payload) {
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
|
@ -107,4 +106,75 @@ public class SdJws {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JWSHeader getHeader() {
|
||||||
|
return this.jwsInput.getHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyIssuedAtClaim() throws VerificationException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
long iat = SdJwtUtils.readTimeClaim(payload, "iat");
|
||||||
|
|
||||||
|
if (now < iat) {
|
||||||
|
throw new VerificationException("jwt issued in the future");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyExpClaim() throws VerificationException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
long exp = SdJwtUtils.readTimeClaim(payload, "exp");
|
||||||
|
|
||||||
|
if (now >= exp) {
|
||||||
|
throw new VerificationException("jwt has expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verifyNotBeforeClaim() throws VerificationException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
long nbf = SdJwtUtils.readTimeClaim(payload, "nbf");
|
||||||
|
|
||||||
|
if (now < nbf) {
|
||||||
|
throw new VerificationException("jwt not valid yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the JWS is not too old.
|
||||||
|
*
|
||||||
|
* @param maxAge Maximum age in seconds
|
||||||
|
* @throws VerificationException if too old
|
||||||
|
*/
|
||||||
|
public void verifyAge(int maxAge) throws VerificationException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
long iat = SdJwtUtils.readTimeClaim(getPayload(), "iat");
|
||||||
|
|
||||||
|
if (now - iat > maxAge) {
|
||||||
|
throw new VerificationException("jwt is too old");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that SD-JWT was issued by one of the provided issuers.
|
||||||
|
* @param issuers List of trusted issuers
|
||||||
|
*/
|
||||||
|
public void verifyIssClaim(List<String> issuers) throws VerificationException {
|
||||||
|
verifyClaimAgainstTrustedValues(issuers, "iss");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that SD-JWT vct claim matches the expected one.
|
||||||
|
* @param vcts list of supported verifiable credential types
|
||||||
|
*/
|
||||||
|
public void verifyVctClaim(List<String> vcts) throws VerificationException {
|
||||||
|
verifyClaimAgainstTrustedValues(vcts, "vct");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyClaimAgainstTrustedValues(List<String> trustedValues, String claimName)
|
||||||
|
throws VerificationException {
|
||||||
|
String claimValue = SdJwtUtils.readClaim(payload, claimName);
|
||||||
|
|
||||||
|
if (!trustedValues.contains(claimValue)) {
|
||||||
|
throw new VerificationException(String.format("Unknown '%s' claim value: %s", claimName, claimValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
||||||
|
|
||||||
|
@ -194,6 +195,19 @@ public class SdJwt {
|
||||||
return disclosures;
|
return disclosures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies SD-JWT as to whether the Issuer-signed JWT's signature and disclosures are valid.
|
||||||
|
*
|
||||||
|
* @param verificationOpts Options to parameterize the Issuer-Signed JWT verification. A verifier
|
||||||
|
* must be specified for validating the Issuer-signed JWT. The caller
|
||||||
|
* is responsible for establishing trust in that associated public keys
|
||||||
|
* belong to the intended issuer.
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
public void verify(IssuerSignedJwtVerificationOpts verificationOpts) throws VerificationException {
|
||||||
|
new SdJwtVerificationContext(issuerSignedJWT, disclosures).verifyIssuance(verificationOpts);
|
||||||
|
}
|
||||||
|
|
||||||
// builder for SdJwt
|
// builder for SdJwt
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private DisclosureSpec disclosureSpec;
|
private DisclosureSpec disclosureSpec;
|
||||||
|
|
|
@ -20,28 +20,35 @@ import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.jose.jws.crypto.HashUtils;
|
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
|
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*/
|
*/
|
||||||
public class SdJwtUtils {
|
public class SdJwtUtils {
|
||||||
|
|
||||||
public static final ObjectMapper mapper = new ObjectMapper();
|
public static final ObjectMapper mapper = new ObjectMapper();
|
||||||
private static SecureRandom RANDOM = new SecureRandom();
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
public static String encodeNoPad(byte[] bytes) {
|
public static String encodeNoPad(byte[] bytes) {
|
||||||
return Base64Url.encode(bytes);
|
return Base64Url.encode(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] decodeNoPad(String encoded) {
|
||||||
|
return Base64Url.decode(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
public static String hashAndBase64EncodeNoPad(byte[] disclosureBytes, String hashAlg) {
|
public static String hashAndBase64EncodeNoPad(byte[] disclosureBytes, String hashAlg) {
|
||||||
return encodeNoPad(HashUtils.hash(hashAlg, disclosureBytes));
|
return encodeNoPad(HashUtils.hash(hashAlg, disclosureBytes));
|
||||||
}
|
}
|
||||||
|
@ -72,6 +79,54 @@ public class SdJwtUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ArrayNode decodeDisclosureString(String disclosure) throws VerificationException {
|
||||||
|
JsonNode jsonNode;
|
||||||
|
|
||||||
|
// Decode Base64URL-encoded disclosure
|
||||||
|
var decoded = new String(decodeNoPad(disclosure));
|
||||||
|
|
||||||
|
// Parse the disclosure string into a JSON array
|
||||||
|
try {
|
||||||
|
jsonNode = mapper.readTree(decoded);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new VerificationException("Disclosure is not a valid JSON", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the parsed JSON is an array
|
||||||
|
if (!jsonNode.isArray()) {
|
||||||
|
throw new VerificationException("Disclosure is not a JSON array");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ArrayNode) jsonNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long readTimeClaim(JsonNode payload, String claimName) throws VerificationException {
|
||||||
|
JsonNode claim = payload.get(claimName);
|
||||||
|
if (claim == null || !claim.isNumber()) {
|
||||||
|
throw new VerificationException("Missing or invalid '" + claimName + "' claim");
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim.asLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readClaim(JsonNode payload, String claimName) throws VerificationException {
|
||||||
|
JsonNode claim = payload.get(claimName);
|
||||||
|
if (claim == null) {
|
||||||
|
throw new VerificationException("Missing '" + claimName + "' claim");
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim.textValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonNode deepClone(JsonNode node) {
|
||||||
|
try {
|
||||||
|
byte[] serializedNode = mapper.writeValueAsBytes(node);
|
||||||
|
return mapper.readTree(serializedNode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static ArraySpacedPrettyPrinter arraySpacedPrettyPrinter = new ArraySpacedPrettyPrinter();
|
static ArraySpacedPrettyPrinter arraySpacedPrettyPrinter = new ArraySpacedPrettyPrinter();
|
||||||
|
|
||||||
static class ArraySpacedPrettyPrinter extends MinimalPrettyPrinter {
|
static class ArraySpacedPrettyPrinter extends MinimalPrettyPrinter {
|
||||||
|
|
|
@ -0,0 +1,730 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
|
||||||
|
import org.keycloak.crypto.ECDSASignatureVerifierContext;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyWrapper;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
import org.keycloak.jose.jwk.JWK;
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
|
import org.keycloak.util.JWKSUtils;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs SD-JWT verification in isolation with only essential properties.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class SdJwtVerificationContext {
|
||||||
|
private String sdJwtVpString;
|
||||||
|
|
||||||
|
private final IssuerSignedJWT issuerSignedJwt;
|
||||||
|
private final Map<String, String> disclosures;
|
||||||
|
private KeyBindingJWT keyBindingJwt;
|
||||||
|
|
||||||
|
public SdJwtVerificationContext(
|
||||||
|
String sdJwtVpString,
|
||||||
|
IssuerSignedJWT issuerSignedJwt,
|
||||||
|
Map<String, String> disclosures,
|
||||||
|
KeyBindingJWT keyBindingJwt) {
|
||||||
|
this(issuerSignedJwt, disclosures);
|
||||||
|
this.keyBindingJwt = keyBindingJwt;
|
||||||
|
this.sdJwtVpString = sdJwtVpString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SdJwtVerificationContext(IssuerSignedJWT issuerSignedJwt, Map<String, String> disclosures) {
|
||||||
|
this.issuerSignedJwt = issuerSignedJwt;
|
||||||
|
this.disclosures = disclosures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SdJwtVerificationContext(IssuerSignedJWT issuerSignedJwt, List<String> disclosureStrings) {
|
||||||
|
this.issuerSignedJwt = issuerSignedJwt;
|
||||||
|
this.disclosures = computeDigestDisclosureMap(disclosureStrings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> computeDigestDisclosureMap(List<String> disclosureStrings) {
|
||||||
|
return disclosureStrings.stream()
|
||||||
|
.map(disclosureString -> {
|
||||||
|
var digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
||||||
|
disclosureString.getBytes(), issuerSignedJwt.getSdHashAlg());
|
||||||
|
return Map.entry(digest, disclosureString);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies SD-JWT as to whether the Issuer-signed JWT's signature and disclosures are valid.
|
||||||
|
*
|
||||||
|
* <p>Upon receiving an SD-JWT, a Holder or a Verifier needs to ensure that:</p>
|
||||||
|
* - the Issuer-signed JWT is valid, i.e., it is signed by the Issuer and the signature is valid, and
|
||||||
|
* - all Disclosures are valid and correspond to a respective digest value in the Issuer-signed JWT
|
||||||
|
* (directly in the payload or recursively included in the contents of other Disclosures).
|
||||||
|
*
|
||||||
|
* @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification. A verifier
|
||||||
|
* must be specified for validating the Issuer-signed JWT. The caller
|
||||||
|
* is responsible for establishing trust in that associated public keys
|
||||||
|
* belong to the intended issuer.
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
public void verifyIssuance(
|
||||||
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
// Validate the Issuer-signed JWT.
|
||||||
|
validateIssuerSignedJwt(issuerSignedJwtVerificationOpts.getVerifier());
|
||||||
|
|
||||||
|
// Validate disclosures.
|
||||||
|
var disclosedPayload = validateDisclosuresDigests();
|
||||||
|
|
||||||
|
// Validate time claims.
|
||||||
|
// Issuers will typically include claims controlling the validity of the SD-JWT in plaintext in the
|
||||||
|
// SD-JWT payload, but there is no guarantee they would do so. Therefore, Verifiers cannot reliably
|
||||||
|
// depend on that and need to operate as though security-critical claims might be selectively disclosable.
|
||||||
|
validateIssuerSignedJwtTimeClaims(disclosedPayload, issuerSignedJwtVerificationOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies SD-JWT presentation.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Upon receiving a Presentation, in addition to the checks in {@link #verifyIssuance}, Verifiers need
|
||||||
|
* to ensure that if Key Binding is required, the Key Binding JWT is signed by the Holder and valid.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification. A verifier
|
||||||
|
* must be specified for validating the Issuer-signed JWT. The caller
|
||||||
|
* is responsible for establishing trust in that associated public keys
|
||||||
|
* belong to the intended issuer.
|
||||||
|
* @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification.
|
||||||
|
* Must, among others, specify the Verifier's policy whether
|
||||||
|
* to check Key Binding.
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
public void verifyPresentation(
|
||||||
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
// If Key Binding is required and a Key Binding JWT is not provided,
|
||||||
|
// the Verifier MUST reject the Presentation.
|
||||||
|
if (keyBindingJwtVerificationOpts.isKeyBindingRequired() && keyBindingJwt == null) {
|
||||||
|
throw new VerificationException("Missing Key Binding JWT");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upon receiving a Presentation, in addition to the checks in {@link #verifyIssuance}...
|
||||||
|
verifyIssuance(issuerSignedJwtVerificationOpts);
|
||||||
|
|
||||||
|
// Validate Key Binding JWT if required
|
||||||
|
if (keyBindingJwtVerificationOpts.isKeyBindingRequired()) {
|
||||||
|
validateKeyBindingJwt(keyBindingJwtVerificationOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Issuer-signed JWT
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Upon receiving an SD-JWT, a Holder or a Verifier needs to ensure that:
|
||||||
|
* - the Issuer-signed JWT is valid, i.e., it is signed by the Issuer and the signature is valid
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void validateIssuerSignedJwt(SignatureVerifierContext verifier) throws VerificationException {
|
||||||
|
// Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure
|
||||||
|
issuerSignedJwt.verifySdHashAlgorithm();
|
||||||
|
|
||||||
|
// Validate the signature over the Issuer-signed JWT
|
||||||
|
try {
|
||||||
|
issuerSignedJwt.verifySignature(verifier);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Invalid Issuer-Signed JWT", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Key Binding JWT
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void validateKeyBindingJwt(
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
// Check that the typ of the Key Binding JWT is kb+jwt
|
||||||
|
validateKeyBindingJwtTyp();
|
||||||
|
|
||||||
|
// Determine the public key for the Holder from the SD-JWT
|
||||||
|
var cnf = issuerSignedJwt.getCnfClaim().orElseThrow(
|
||||||
|
() -> new VerificationException("No cnf claim in Issuer-signed JWT for key binding")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure that a signing algorithm was used that was deemed secure for the application.
|
||||||
|
// The none algorithm MUST NOT be accepted.
|
||||||
|
var holderVerifier = buildHolderVerifier(cnf);
|
||||||
|
|
||||||
|
// Validate the signature over the Key Binding JWT
|
||||||
|
try {
|
||||||
|
keyBindingJwt.verifySignature(holderVerifier);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Key binding JWT invalid", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the creation time of the Key Binding JWT is within an acceptable window.
|
||||||
|
validateKeyBindingJwtTimeClaims(keyBindingJwtVerificationOpts);
|
||||||
|
|
||||||
|
// Determine that the Key Binding JWT is bound to the current transaction and was created
|
||||||
|
// for this Verifier (replay protection) by validating nonce and aud claims.
|
||||||
|
preventKeyBindingJwtReplay(keyBindingJwtVerificationOpts);
|
||||||
|
|
||||||
|
// The same hash algorithm as for the Disclosures MUST be used (defined by the _sd_alg element
|
||||||
|
// in the Issuer-signed JWT or the default value, as defined in Section 5.1.1).
|
||||||
|
validateKeyBindingJwtSdHashIntegrity();
|
||||||
|
|
||||||
|
// Check that the Key Binding JWT is a valid JWT in all other respects
|
||||||
|
// -> Covered in part by `keyBindingJwt` being an instance of SdJws?
|
||||||
|
// -> Time claims are checked above
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Key Binding JWT's typ header attribute
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void validateKeyBindingJwtTyp() throws VerificationException {
|
||||||
|
var typ = keyBindingJwt.getHeader().getType();
|
||||||
|
if (!typ.equals(KeyBindingJWT.TYP)) {
|
||||||
|
throw new VerificationException("Key Binding JWT is not of declared typ " + KeyBindingJWT.TYP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build holder verifier from JWK node.
|
||||||
|
*
|
||||||
|
* @throws VerificationException if unable
|
||||||
|
*/
|
||||||
|
private SignatureVerifierContext buildHolderVerifier(JsonNode cnf) throws VerificationException {
|
||||||
|
Objects.requireNonNull(cnf);
|
||||||
|
|
||||||
|
// Read JWK
|
||||||
|
var cnfJwk = cnf.get("jwk");
|
||||||
|
if (cnfJwk == null) {
|
||||||
|
throw new UnsupportedOperationException("Only cnf/jwk claim supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JWK
|
||||||
|
KeyWrapper keyWrapper;
|
||||||
|
try {
|
||||||
|
JWK jwk = SdJwtUtils.mapper.convertValue(cnfJwk, JWK.class);
|
||||||
|
keyWrapper = JWKSUtils.getKeyWrapper(jwk);
|
||||||
|
Objects.requireNonNull(keyWrapper);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new VerificationException("Malformed or unsupported cnf/jwk claim");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build verifier
|
||||||
|
|
||||||
|
// KeyType.EC
|
||||||
|
if (keyWrapper.getType().equals(KeyType.EC)) {
|
||||||
|
if (keyWrapper.getAlgorithm() == null) {
|
||||||
|
Objects.requireNonNull(keyWrapper.getCurve());
|
||||||
|
|
||||||
|
String alg = null;
|
||||||
|
switch (keyWrapper.getCurve()) {
|
||||||
|
case "P-256":
|
||||||
|
alg = "ES256";
|
||||||
|
break;
|
||||||
|
case "P-384":
|
||||||
|
alg = "ES384";
|
||||||
|
break;
|
||||||
|
case "P-521":
|
||||||
|
alg = "ES512";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyWrapper.setAlgorithm(alg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ECDSASignatureVerifierContext(keyWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyType.RSA
|
||||||
|
if (keyWrapper.getType().equals(KeyType.RSA)) {
|
||||||
|
return new AsymmetricSignatureVerifierContext(keyWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyType is not supported
|
||||||
|
// This is unreachable as of now given that `JWKSUtils.getKeyWrapper` will fail
|
||||||
|
// on JWKs with key type not equal to EC or RSA.
|
||||||
|
throw new VerificationException("cnf/jwk alg is unsupported or deemed not secure");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Issuer-Signed JWT time claims.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Check that the SD-JWT is valid using claims such as nbf, iat, and exp in the processed payload.
|
||||||
|
* If a required validity-controlling claim is missing, the SD-JWT MUST be rejected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void validateIssuerSignedJwtTimeClaims(
|
||||||
|
JsonNode payload,
|
||||||
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (issuerSignedJwtVerificationOpts.mustValidateIssuedAtClaim()
|
||||||
|
&& now < SdJwtUtils.readTimeClaim(payload, "iat")) {
|
||||||
|
throw new VerificationException("JWT issued in the future");
|
||||||
|
}
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Issuer-Signed JWT: Invalid `iat` claim", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (issuerSignedJwtVerificationOpts.mustValidateExpirationClaim()
|
||||||
|
&& now >= SdJwtUtils.readTimeClaim(payload, "exp")) {
|
||||||
|
throw new VerificationException("JWT has expired");
|
||||||
|
}
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Issuer-Signed JWT: Invalid `exp` claim", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (issuerSignedJwtVerificationOpts.mustValidateNotBeforeClaim()
|
||||||
|
&& now < SdJwtUtils.readTimeClaim(payload, "nbf")) {
|
||||||
|
throw new VerificationException("JWT is not yet valid");
|
||||||
|
}
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Issuer-Signed JWT: Invalid `nbf` claim", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate key binding JWT time claims.
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void validateKeyBindingJwtTimeClaims(
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
// Check that the creation time of the Key Binding JWT, as determined by the iat claim,
|
||||||
|
// is within an acceptable window
|
||||||
|
|
||||||
|
try {
|
||||||
|
keyBindingJwt.verifyIssuedAtClaim();
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Key binding JWT: Invalid `iat` claim", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
keyBindingJwt.verifyAge(keyBindingJwtVerificationOpts.getAllowedMaxAge());
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Key binding JWT is too old");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check other time claims
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (keyBindingJwtVerificationOpts.mustValidateExpirationClaim()) {
|
||||||
|
keyBindingJwt.verifyExpClaim();
|
||||||
|
}
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Key binding JWT: Invalid `exp` claim", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (keyBindingJwtVerificationOpts.mustValidateNotBeforeClaim()) {
|
||||||
|
keyBindingJwt.verifyNotBeforeClaim();
|
||||||
|
}
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new VerificationException("Key binding JWT: Invalid `nbf` claim", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate disclosures' digests
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Upon receiving an SD-JWT, a Holder or a Verifier needs to ensure that:
|
||||||
|
* - all Disclosures are valid and correspond to a respective digest value in the Issuer-signed JWT
|
||||||
|
* (directly in the payload or recursively included in the contents of other Disclosures)
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* We additionally check that salt values are not reused:
|
||||||
|
* The salt value MUST be unique for each claim that is to be selectively disclosed.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the fully disclosed SdJwt payload
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private JsonNode validateDisclosuresDigests() throws VerificationException {
|
||||||
|
// Validate SdJwt digests by attempting full recursive disclosing.
|
||||||
|
Set<String> visitedSalts = new HashSet<>();
|
||||||
|
Set<String> visitedDigests = new HashSet<>();
|
||||||
|
Set<String> visitedDisclosureStrings = new HashSet<>();
|
||||||
|
var disclosedPayload = validateViaRecursiveDisclosing(
|
||||||
|
SdJwtUtils.deepClone(issuerSignedJwt.getPayload()),
|
||||||
|
visitedSalts, visitedDigests, visitedDisclosureStrings);
|
||||||
|
|
||||||
|
// Validate all disclosures where visited
|
||||||
|
validateDisclosuresVisits(visitedDisclosureStrings);
|
||||||
|
|
||||||
|
return disclosedPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate SdJwt digests by attempting full recursive disclosing.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* By recursively disclosing all disclosable fields in the SdJwt payload, validation rules are
|
||||||
|
* enforced regarding the conformance of linked disclosures. Additional rules should be enforced
|
||||||
|
* after calling this method based on the visited data arguments.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the fully disclosed SdJwt payload
|
||||||
|
*/
|
||||||
|
private JsonNode validateViaRecursiveDisclosing(
|
||||||
|
JsonNode currentNode,
|
||||||
|
Set<String> visitedSalts,
|
||||||
|
Set<String> visitedDigests,
|
||||||
|
Set<String> visitedDisclosureStrings
|
||||||
|
) throws VerificationException {
|
||||||
|
if (!currentNode.isObject() && !currentNode.isArray()) {
|
||||||
|
return currentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all objects having an _sd key that refers to an array of strings.
|
||||||
|
if (currentNode.isObject()) {
|
||||||
|
var currentObjectNode = ((ObjectNode) currentNode);
|
||||||
|
|
||||||
|
var sdArray = currentObjectNode.get(IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE);
|
||||||
|
if (sdArray != null && sdArray.isArray()) {
|
||||||
|
for (var el : sdArray) {
|
||||||
|
if (!el.isTextual()) {
|
||||||
|
throw new VerificationException(
|
||||||
|
"Unexpected non-string element inside _sd array: " + el
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
||||||
|
// If no such Disclosure can be found, the digest MUST be ignored.
|
||||||
|
|
||||||
|
var digest = el.asText();
|
||||||
|
markDigestAsVisited(digest, visitedDigests);
|
||||||
|
var disclosure = disclosures.get(digest);
|
||||||
|
|
||||||
|
if (disclosure != null) {
|
||||||
|
// Mark disclosure as visited
|
||||||
|
visitedDisclosureStrings.add(disclosure);
|
||||||
|
|
||||||
|
// Validate disclosure format
|
||||||
|
var decodedDisclosure = validateSdArrayDigestDisclosureFormat(disclosure);
|
||||||
|
|
||||||
|
// Mark salt as visited
|
||||||
|
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
||||||
|
|
||||||
|
// Insert, at the level of the _sd key, a new claim using the claim name
|
||||||
|
// and claim value from the Disclosure
|
||||||
|
currentObjectNode.set(
|
||||||
|
decodedDisclosure.getClaimName(),
|
||||||
|
decodedDisclosure.getClaimValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all _sd keys and their contents from the Issuer-signed JWT payload.
|
||||||
|
// If this results in an object with no properties, it should be represented as an empty object {}
|
||||||
|
currentObjectNode.remove(IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE);
|
||||||
|
|
||||||
|
// Remove the claim _sd_alg from the SD-JWT payload.
|
||||||
|
currentObjectNode.remove(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all array elements that are objects with one key, that key being ... and referring to a string
|
||||||
|
if (currentNode.isArray()) {
|
||||||
|
var currentArrayNode = ((ArrayNode) currentNode);
|
||||||
|
var indexesToRemove = new ArrayList<Integer>();
|
||||||
|
|
||||||
|
for (int i = 0; i < currentArrayNode.size(); ++i) {
|
||||||
|
var itemNode = currentArrayNode.get(i);
|
||||||
|
if (itemNode.isObject() && itemNode.size() == 1) {
|
||||||
|
// Check single "..." field
|
||||||
|
var field = itemNode.fields().next();
|
||||||
|
if (field.getKey().equals(UndisclosedArrayElement.SD_CLAIM_NAME)
|
||||||
|
&& field.getValue().isTextual()) {
|
||||||
|
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
||||||
|
// If no such Disclosure can be found, the digest MUST be ignored.
|
||||||
|
|
||||||
|
var digest = field.getValue().asText();
|
||||||
|
markDigestAsVisited(digest, visitedDigests);
|
||||||
|
var disclosure = disclosures.get(digest);
|
||||||
|
|
||||||
|
if (disclosure != null) {
|
||||||
|
// Mark disclosure as visited
|
||||||
|
visitedDisclosureStrings.add(disclosure);
|
||||||
|
|
||||||
|
// Validate disclosure format
|
||||||
|
var decodedDisclosure = validateArrayElementDigestDisclosureFormat(disclosure);
|
||||||
|
|
||||||
|
// Mark salt as visited
|
||||||
|
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
||||||
|
|
||||||
|
// Replace the array element with the value from the Disclosure.
|
||||||
|
// Removal is done below.
|
||||||
|
currentArrayNode.set(i, decodedDisclosure.getClaimValue());
|
||||||
|
} else {
|
||||||
|
// Remove all array elements for which the digest was not found in the previous step.
|
||||||
|
indexesToRemove.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all array elements for which the digest was not found in the previous step.
|
||||||
|
indexesToRemove.forEach(currentArrayNode::remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (JsonNode childNode : currentNode) {
|
||||||
|
validateViaRecursiveDisclosing(childNode, visitedSalts, visitedDigests, visitedDisclosureStrings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark digest as visited.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If any digest value is encountered more than once in the Issuer-signed JWT payload
|
||||||
|
* (directly or recursively via other Disclosures), the SD-JWT MUST be rejected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if not first visit
|
||||||
|
*/
|
||||||
|
private void markDigestAsVisited(String digest, Set<String> visitedDigests)
|
||||||
|
throws VerificationException {
|
||||||
|
if (!visitedDigests.add(digest)) {
|
||||||
|
// If add returns false, then it is a duplicate
|
||||||
|
throw new VerificationException("A digest was encountered more than once: " + digest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark salt as visited.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The salt value MUST be unique for each claim that is to be selectively disclosed.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if not first visit
|
||||||
|
*/
|
||||||
|
private void markSaltAsVisited(String salt, Set<String> visitedSalts)
|
||||||
|
throws VerificationException {
|
||||||
|
if (!visitedSalts.add(salt)) {
|
||||||
|
// If add returns false, then it is a duplicate
|
||||||
|
throw new VerificationException("A salt value was reused: " + salt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate disclosure assuming digest was found in an object's _sd key.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the contents of the respective Disclosure is not a JSON-encoded array of three elements
|
||||||
|
* (salt, claim name, claim value), the SD-JWT MUST be rejected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the claim name is _sd or ..., the SD-JWT MUST be rejected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return decoded disclosure (salt, claim name, claim value)
|
||||||
|
*/
|
||||||
|
private DisclosureFields validateSdArrayDigestDisclosureFormat(String disclosure)
|
||||||
|
throws VerificationException {
|
||||||
|
ArrayNode arrayNode = SdJwtUtils.decodeDisclosureString(disclosure);
|
||||||
|
|
||||||
|
// Check if the array has exactly three elements
|
||||||
|
if (arrayNode.size() != 3) {
|
||||||
|
throw new VerificationException("A field disclosure must contain exactly three elements");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the claim name is _sd or ..., the SD-JWT MUST be rejected.
|
||||||
|
|
||||||
|
var denylist = List.of(
|
||||||
|
IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE,
|
||||||
|
UndisclosedArrayElement.SD_CLAIM_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
String claimName = arrayNode.get(1).asText();
|
||||||
|
if (denylist.contains(claimName)) {
|
||||||
|
throw new VerificationException("Disclosure claim name must not be '_sd' or '...'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return decoded disclosure
|
||||||
|
return new DisclosureFields(
|
||||||
|
arrayNode.get(0).asText(),
|
||||||
|
claimName,
|
||||||
|
arrayNode.get(2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate disclosure assuming digest was found as an undisclosed array element.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the contents of the respective Disclosure is not a JSON-encoded array of
|
||||||
|
* two elements (salt, value), the SD-JWT MUST be rejected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return decoded disclosure (salt, value)
|
||||||
|
*/
|
||||||
|
private DisclosureFields validateArrayElementDigestDisclosureFormat(String disclosure)
|
||||||
|
throws VerificationException {
|
||||||
|
ArrayNode arrayNode = SdJwtUtils.decodeDisclosureString(disclosure);
|
||||||
|
|
||||||
|
// Check if the array has exactly two elements
|
||||||
|
if (arrayNode.size() != 2) {
|
||||||
|
throw new VerificationException("An array element disclosure must contain exactly two elements");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return decoded disclosure
|
||||||
|
return new DisclosureFields(
|
||||||
|
arrayNode.get(0).asText(),
|
||||||
|
null,
|
||||||
|
arrayNode.get(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate all disclosures where visited
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If any Disclosure was not referenced by digest value in the Issuer-signed JWT (directly or recursively via
|
||||||
|
* other Disclosures), the SD-JWT MUST be rejected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if not the case
|
||||||
|
*/
|
||||||
|
private void validateDisclosuresVisits(Set<String> visitedDisclosureStrings)
|
||||||
|
throws VerificationException {
|
||||||
|
if (visitedDisclosureStrings.size() < disclosures.size()) {
|
||||||
|
throw new VerificationException("At least one disclosure is not protected by digest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run checks for replay protection.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Determine that the Key Binding JWT is bound to the current transaction and was created for this
|
||||||
|
* Verifier (replay protection) by validating nonce and aud claims.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void preventKeyBindingJwtReplay(
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
JsonNode nonce = keyBindingJwt.getPayload().get("nonce");
|
||||||
|
if (nonce == null || !nonce.isTextual()
|
||||||
|
|| !nonce.asText().equals(keyBindingJwtVerificationOpts.getNonce())) {
|
||||||
|
throw new VerificationException("Key binding JWT: Unexpected `nonce` value");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode aud = keyBindingJwt.getPayload().get("aud");
|
||||||
|
if (aud == null || !aud.isTextual()
|
||||||
|
|| !aud.asText().equals(keyBindingJwtVerificationOpts.getAud())) {
|
||||||
|
throw new VerificationException("Key binding JWT: Unexpected `aud` value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate integrity of Key Binding JWT's sd_hash.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Calculate the digest over the Issuer-signed JWT and Disclosures and verify that it matches
|
||||||
|
* the value of the sd_hash claim in the Key Binding JWT.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
private void validateKeyBindingJwtSdHashIntegrity() throws VerificationException {
|
||||||
|
Objects.requireNonNull(sdJwtVpString);
|
||||||
|
|
||||||
|
JsonNode sdHash = keyBindingJwt.getPayload().get("sd_hash");
|
||||||
|
if (sdHash == null || !sdHash.isTextual()) {
|
||||||
|
throw new VerificationException("Key binding JWT: Claim `sd_hash` missing or not a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastDelimiterIndex = sdJwtVpString.lastIndexOf(SdJwt.DELIMITER);
|
||||||
|
String toHash = sdJwtVpString.substring(0, lastDelimiterIndex + 1);
|
||||||
|
|
||||||
|
String digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
||||||
|
toHash.getBytes(), issuerSignedJwt.getSdHashAlg());
|
||||||
|
|
||||||
|
if (!digest.equals(sdHash.asText())) {
|
||||||
|
throw new VerificationException("Key binding JWT: Invalid `sd_hash` digest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain record for disclosure fields.
|
||||||
|
*/
|
||||||
|
private static class DisclosureFields {
|
||||||
|
String saltValue;
|
||||||
|
String claimName;
|
||||||
|
JsonNode claimValue;
|
||||||
|
|
||||||
|
public DisclosureFields(String saltValue, String claimName, JsonNode claimValue) {
|
||||||
|
this.saltValue = saltValue;
|
||||||
|
this.claimName = claimName;
|
||||||
|
this.claimValue = claimValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSaltValue() {
|
||||||
|
return saltValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClaimName() {
|
||||||
|
return claimName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonNode getClaimValue() {
|
||||||
|
return claimValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*/
|
*/
|
||||||
public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayElement {
|
public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayElement {
|
||||||
|
public static final String SD_CLAIM_NAME = "...";
|
||||||
private final JsonNode arrayElement;
|
private final JsonNode arrayElement;
|
||||||
|
|
||||||
private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) {
|
private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) {
|
||||||
|
@ -34,7 +35,7 @@ public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayEl
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JsonNode getVisibleValue(String hashAlg) {
|
public JsonNode getVisibleValue(String hashAlg) {
|
||||||
return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg));
|
return SdJwtUtils.mapper.createObjectNode().put(SD_CLAIM_NAME, getDisclosureDigest(hashAlg));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -23,19 +23,24 @@ import org.keycloak.sdjwt.SdJws;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class KeyBindingJWT extends SdJws {
|
public class KeyBindingJWT extends SdJws {
|
||||||
|
|
||||||
|
public static final String TYP = "kb+jwt";
|
||||||
|
|
||||||
|
public KeyBindingJWT(JsonNode payload, SignatureSignerContext signer, String jwsType) {
|
||||||
|
super(payload, signer, jwsType);
|
||||||
|
}
|
||||||
|
|
||||||
public static KeyBindingJWT of(String jwsString) {
|
public static KeyBindingJWT of(String jwsString) {
|
||||||
return new KeyBindingJWT(jwsString);
|
return new KeyBindingJWT(jwsString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static KeyBindingJWT from(JsonNode payload, SignatureSignerContext signer, String jwsType) {
|
public static KeyBindingJWT from(JsonNode payload, SignatureSignerContext signer, String jwsType) {
|
||||||
JWSInput jwsInput = sign(payload, signer, jwsType);
|
return new KeyBindingJWT(payload, signer, jwsType);
|
||||||
return new KeyBindingJWT(payload, jwsInput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeyBindingJWT(JsonNode payload, JWSInput jwsInput) {
|
private KeyBindingJWT(JsonNode payload, JWSInput jwsInput) {
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for Key Binding JWT verification.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class KeyBindingJwtVerificationOpts {
|
||||||
|
/**
|
||||||
|
* Specifies the Verifier's policy whether to check Key Binding
|
||||||
|
*/
|
||||||
|
private final boolean keyBindingRequired;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the maximum age (in seconds) of an issued Key Binding
|
||||||
|
*/
|
||||||
|
private final int allowedMaxAge;
|
||||||
|
|
||||||
|
private final String nonce;
|
||||||
|
private final String aud;
|
||||||
|
|
||||||
|
private final boolean validateExpirationClaim;
|
||||||
|
private final boolean validateNotBeforeClaim;
|
||||||
|
|
||||||
|
public KeyBindingJwtVerificationOpts(
|
||||||
|
boolean keyBindingRequired,
|
||||||
|
int allowedMaxAge,
|
||||||
|
String nonce,
|
||||||
|
String aud,
|
||||||
|
boolean validateExpirationClaim,
|
||||||
|
boolean validateNotBeforeClaim) {
|
||||||
|
this.keyBindingRequired = keyBindingRequired;
|
||||||
|
this.allowedMaxAge = allowedMaxAge;
|
||||||
|
this.nonce = nonce;
|
||||||
|
this.aud = aud;
|
||||||
|
this.validateExpirationClaim = validateExpirationClaim;
|
||||||
|
this.validateNotBeforeClaim = validateNotBeforeClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isKeyBindingRequired() {
|
||||||
|
return keyBindingRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAllowedMaxAge() {
|
||||||
|
return allowedMaxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNonce() {
|
||||||
|
return nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAud() {
|
||||||
|
return aud;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mustValidateExpirationClaim() {
|
||||||
|
return validateExpirationClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mustValidateNotBeforeClaim() {
|
||||||
|
return validateNotBeforeClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KeyBindingJwtVerificationOpts.Builder builder() {
|
||||||
|
return new KeyBindingJwtVerificationOpts.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private boolean keyBindingRequired = true;
|
||||||
|
private int allowedMaxAge = 5 * 60;
|
||||||
|
private String nonce;
|
||||||
|
private String aud;
|
||||||
|
private boolean validateExpirationClaim = true;
|
||||||
|
private boolean validateNotBeforeClaim = true;
|
||||||
|
|
||||||
|
public Builder withKeyBindingRequired(boolean keyBindingRequired) {
|
||||||
|
this.keyBindingRequired = keyBindingRequired;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAllowedMaxAge(int allowedMaxAge) {
|
||||||
|
this.allowedMaxAge = allowedMaxAge;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withNonce(String nonce) {
|
||||||
|
this.nonce = nonce;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withAud(String aud) {
|
||||||
|
this.aud = aud;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withValidateExpirationClaim(boolean validateExpirationClaim) {
|
||||||
|
this.validateExpirationClaim = validateExpirationClaim;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withValidateNotBeforeClaim(boolean validateNotBeforeClaim) {
|
||||||
|
this.validateNotBeforeClaim = validateNotBeforeClaim;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyBindingJwtVerificationOpts build() {
|
||||||
|
if (keyBindingRequired && (aud == null || nonce == null || nonce.isEmpty())) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Missing `nonce` and `aud` claims for replay protection"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeyBindingJwtVerificationOpts(
|
||||||
|
keyBindingRequired,
|
||||||
|
allowedMaxAge,
|
||||||
|
nonce,
|
||||||
|
aud,
|
||||||
|
validateExpirationClaim,
|
||||||
|
validateNotBeforeClaim
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,11 +27,14 @@ import java.util.Map.Entry;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.sdjwt.IssuerSignedJWT;
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
||||||
import org.keycloak.sdjwt.SdJwt;
|
import org.keycloak.sdjwt.SdJwt;
|
||||||
import org.keycloak.sdjwt.SdJwtUtils;
|
import org.keycloak.sdjwt.SdJwtUtils;
|
||||||
|
import org.keycloak.sdjwt.SdJwtVerificationContext;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
@ -41,7 +44,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*/
|
*/
|
||||||
public class SdJwtVP {
|
public class SdJwtVP {
|
||||||
private String sdJwtVpString;
|
private final String sdJwtVpString;
|
||||||
private final IssuerSignedJWT issuerSignedJWT;
|
private final IssuerSignedJWT issuerSignedJWT;
|
||||||
|
|
||||||
private final Map<String, ArrayNode> claims;
|
private final Map<String, ArrayNode> claims;
|
||||||
|
@ -52,6 +55,10 @@ public class SdJwtVP {
|
||||||
|
|
||||||
private final Optional<KeyBindingJWT> keyBindingJWT;
|
private final Optional<KeyBindingJWT> keyBindingJWT;
|
||||||
|
|
||||||
|
public Map<String, ArrayNode> getClaims() {
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
public IssuerSignedJWT getIssuerSignedJWT() {
|
public IssuerSignedJWT getIssuerSignedJWT() {
|
||||||
return issuerSignedJWT;
|
return issuerSignedJWT;
|
||||||
}
|
}
|
||||||
|
@ -171,7 +178,7 @@ public class SdJwtVP {
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonNode getCnfClaim() {
|
public JsonNode getCnfClaim() {
|
||||||
return issuerSignedJWT.getPayload().get("cnf");
|
return issuerSignedJWT.getCnfClaim().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String present(List<String> disclosureDigests, JsonNode keyBindingClaims,
|
public String present(List<String> disclosureDigests, JsonNode keyBindingClaims,
|
||||||
|
@ -195,11 +202,31 @@ public class SdJwtVP {
|
||||||
String sd_hash = SdJwtUtils.hashAndBase64EncodeNoPad(unboundPresentation.getBytes(), getHashAlgorithm());
|
String sd_hash = SdJwtUtils.hashAndBase64EncodeNoPad(unboundPresentation.getBytes(), getHashAlgorithm());
|
||||||
keyBindingClaims = ((ObjectNode) keyBindingClaims).put("sd_hash", sd_hash);
|
keyBindingClaims = ((ObjectNode) keyBindingClaims).put("sd_hash", sd_hash);
|
||||||
KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(keyBindingClaims, holdSignatureSignerContext, jwsType);
|
KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(keyBindingClaims, holdSignatureSignerContext, jwsType);
|
||||||
sb.append(keyBindingJWT.getJwsString());
|
sb.append(keyBindingJWT.toJws());
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively seraches the node with the given value.
|
/**
|
||||||
|
* Verifies SD-JWT presentation.
|
||||||
|
*
|
||||||
|
* @param issuerSignedJwtVerificationOpts Options to parameterize the verification. A verifier must be specified
|
||||||
|
* for validating the Issuer-signed JWT. The caller is responsible for
|
||||||
|
* establishing trust in that associated public keys belong to the
|
||||||
|
* intended issuer.
|
||||||
|
* @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification.
|
||||||
|
* Must, among others, specify the Verifier's policy whether
|
||||||
|
* to check Key Binding.
|
||||||
|
* @throws VerificationException if verification failed
|
||||||
|
*/
|
||||||
|
public void verify(
|
||||||
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
new SdJwtVerificationContext(sdJwtVpString, issuerSignedJWT, disclosures, keyBindingJWT.orElse(null))
|
||||||
|
.verifyPresentation(issuerSignedJwtVerificationOpts, keyBindingJwtVerificationOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively searches the node with the given value.
|
||||||
// Returns the node if found, null otherwise.
|
// Returns the node if found, null otherwise.
|
||||||
private static JsonNode findNode(JsonNode node, String value) {
|
private static JsonNode findNode(JsonNode node, String value) {
|
||||||
if (node == null) {
|
if (node == null) {
|
||||||
|
@ -262,4 +289,4 @@ public class SdJwtVP {
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
183
core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java
Normal file
183
core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
public abstract class SdJwsTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
static TestSettings testSettings = TestSettings.getInstance();
|
||||||
|
|
||||||
|
private JsonNode createPayload() {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
ObjectNode node = mapper.createObjectNode();
|
||||||
|
node.put("sub", "test");
|
||||||
|
node.put("exp", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
||||||
|
node.put("name", "Test User");
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifySignature_Positive() throws Exception {
|
||||||
|
SdJws sdJws = new SdJws(createPayload(), testSettings.holderSigContext, "jwt") {
|
||||||
|
};
|
||||||
|
sdJws.verifySignature(testSettings.holderVerifierContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifySignature_WrongPublicKey() {
|
||||||
|
SdJws sdJws = new SdJws(createPayload(), testSettings.holderSigContext, "jwt") {
|
||||||
|
};
|
||||||
|
assertThrows(VerificationException.class, () -> sdJws.verifySignature(testSettings.issuerVerifierContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyExpClaim_ExpiredJWT() {
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("exp", Instant.now().minus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
||||||
|
SdJws sdJws = new SdJws(payload) {
|
||||||
|
};
|
||||||
|
assertThrows(VerificationException.class, sdJws::verifyExpClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyExpClaim_Positive() throws Exception {
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("exp", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
||||||
|
SdJws sdJws = new SdJws(payload) {
|
||||||
|
};
|
||||||
|
sdJws.verifyExpClaim();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyNotBeforeClaim_Negative() {
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("nbf", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
||||||
|
SdJws sdJws = new SdJws(payload) {
|
||||||
|
};
|
||||||
|
assertThrows(VerificationException.class, sdJws::verifyNotBeforeClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyNotBeforeClaim_Positive() throws Exception {
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("nbf", Instant.now().minus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
||||||
|
SdJws sdJws = new SdJws(payload) {
|
||||||
|
};
|
||||||
|
sdJws.verifyNotBeforeClaim();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPayloadJwsConstruction() {
|
||||||
|
SdJws sdJws = new SdJws(createPayload()) {
|
||||||
|
};
|
||||||
|
assertNotNull(sdJws.getPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalStateException.class)
|
||||||
|
public void testUnsignedJwsConstruction() {
|
||||||
|
SdJws sdJws = new SdJws(createPayload()) {
|
||||||
|
};
|
||||||
|
sdJws.toJws();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSignedJwsConstruction() {
|
||||||
|
SdJws sdJws = new SdJws(createPayload(), testSettings.holderSigContext, "jwt") {
|
||||||
|
};
|
||||||
|
assertNotNull(sdJws.toJws());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyIssClaim_Negative() {
|
||||||
|
List<String> allowedIssuers = List.of("issuer1@sdjwt.com", "issuer2@sdjwt.com");
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("iss", "unknown-issuer@sdjwt.com");
|
||||||
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
|
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyIssClaim(allowedIssuers));
|
||||||
|
assertEquals("Unknown 'iss' claim value: unknown-issuer@sdjwt.com", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyIssClaim_Positive() throws VerificationException {
|
||||||
|
List<String> allowedIssuers = List.of("issuer1@sdjwt.com", "issuer2@sdjwt.com");
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("iss", "issuer1@sdjwt.com");
|
||||||
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
|
sdJws.verifyIssClaim(allowedIssuers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyVctClaim_Negative() {
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("vct", "IdentityCredential");
|
||||||
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
|
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyVctClaim(List.of("PassportCredential")));
|
||||||
|
assertEquals("Unknown 'vct' claim value: IdentityCredential", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyVctClaim_Positive() throws VerificationException {
|
||||||
|
JsonNode payload = createPayload();
|
||||||
|
((ObjectNode) payload).put("vct", "IdentityCredential");
|
||||||
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
|
sdJws.verifyVctClaim(List.of("IdentityCredential"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldValidateAgeSinceIssued() throws VerificationException {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
var sdJws = exampleSdJws(now);
|
||||||
|
sdJws.verifyAge(180);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
var sdJws = exampleSdJws(now - 1000); // that will be too old
|
||||||
|
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180));
|
||||||
|
assertEquals("jwt is too old", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJws exampleSdJws(long iat) {
|
||||||
|
var payload = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
payload.set("iat", SdJwtUtils.mapper.valueToTree(iat));
|
||||||
|
|
||||||
|
return new SdJws(payload) {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
437
core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java
Normal file
437
core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java
Normal file
|
@ -0,0 +1,437 @@
|
||||||
|
/*
|
||||||
|
* 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.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.endsWith;
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
static ObjectMapper mapper = new ObjectMapper();
|
||||||
|
static TestSettings testSettings = TestSettings.getInstance();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void settingsTest() {
|
||||||
|
var issuerSignerContext = testSettings.issuerSigContext;
|
||||||
|
assertNotNull(issuerSignerContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
|
||||||
|
for (String hashAlg : List.of("sha-256", "sha-384", "sha-512")) {
|
||||||
|
var sdJwt = exampleFlatSdJwtV1()
|
||||||
|
.withHashAlgorithm(hashAlg)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
|
||||||
|
var sdJwt = exampleFlatSdJwtV1().build();
|
||||||
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException {
|
||||||
|
var sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
||||||
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception {
|
||||||
|
var sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
||||||
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSdJwtVerification_RecursiveSdJwt() throws Exception {
|
||||||
|
var sdJwt = exampleRecursiveSdJwtV1().build();
|
||||||
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_OnInsecureHashAlg() {
|
||||||
|
var sdJwt = exampleFlatSdJwtV1()
|
||||||
|
.withHashAlgorithm("sha-224") // not deemed secure
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Unexpected or insecure hash algorithm: sha-224", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_WithWrongVerifier() {
|
||||||
|
var sdJwt = exampleFlatSdJwtV1().build();
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
|
.withVerifier(testSettings.holderVerifierContext) // wrong verifier
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage(), is("Invalid Issuer-Signed JWT"));
|
||||||
|
assertThat(exception.getCause().getMessage(), endsWith("Invalid jws signature"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfExpired() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("exp", now - 1000); // expired 1000 seconds ago
|
||||||
|
|
||||||
|
// Exp claim is plain
|
||||||
|
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
|
// Exp claim is undisclosed
|
||||||
|
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
|
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
||||||
|
.withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.build()).build();
|
||||||
|
|
||||||
|
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
|
.withValidateExpirationClaim(true)
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
|
||||||
|
assertEquals("JWT has expired", exception.getCause().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfExpired_CaseExpInvalid() {
|
||||||
|
// exp: null
|
||||||
|
ObjectNode claimSet1 = mapper.createObjectNode();
|
||||||
|
claimSet1.put("given_name", "John");
|
||||||
|
|
||||||
|
// exp: invalid
|
||||||
|
ObjectNode claimSet2 = mapper.createObjectNode();
|
||||||
|
claimSet1.put("given_name", "John");
|
||||||
|
claimSet1.put("exp", "should-not-be-a-string");
|
||||||
|
|
||||||
|
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
||||||
|
var sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
||||||
|
|
||||||
|
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
|
.withValidateExpirationClaim(true)
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
|
||||||
|
assertEquals("Missing or invalid 'exp' claim", exception.getCause().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfIssuedInTheFuture() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("iat", now + 1000); // issued in the future
|
||||||
|
|
||||||
|
// Exp claim is plain
|
||||||
|
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
|
// Exp claim is undisclosed
|
||||||
|
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
|
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
||||||
|
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.build()).build();
|
||||||
|
|
||||||
|
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
|
.withValidateIssuedAtClaim(true)
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Issuer-Signed JWT: Invalid `iat` claim", exception.getMessage());
|
||||||
|
assertEquals("JWT issued in the future", exception.getCause().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfNbfInvalid() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("nbf", now + 1000); // now will be too soon to accept the jwt
|
||||||
|
|
||||||
|
// Exp claim is plain
|
||||||
|
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
|
// Exp claim is undisclosed
|
||||||
|
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
|
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
||||||
|
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.build()).build();
|
||||||
|
|
||||||
|
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
|
.withValidateNotBeforeClaim(true)
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Issuer-Signed JWT: Invalid `nbf` claim", exception.getMessage());
|
||||||
|
assertEquals("JWT is not yet valid", exception.getCause().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfSdArrayElementIsNotString() throws JsonProcessingException {
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.set("_sd", mapper.readTree("[123]"));
|
||||||
|
|
||||||
|
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Unexpected non-string element inside _sd array: 123", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() {
|
||||||
|
for (String forbiddenClaimName : List.of("_sd", "...")) {
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put(forbiddenClaimName, "Value");
|
||||||
|
|
||||||
|
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim(forbiddenClaimName, "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.build()).build();
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Disclosure claim name must not be '_sd' or '...'", exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfDuplicateDigestValue() {
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("given_name", "John"); // this same field will also be nested
|
||||||
|
|
||||||
|
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||||
|
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||||
|
.build()).build();
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().startsWith("A digest was encountered more than once:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sdJwtVerificationShouldFail_IfDuplicateSaltValue() {
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("family_name", "Doe");
|
||||||
|
|
||||||
|
var salt = "eluV5Og3gSNII8EYnsxA_A";
|
||||||
|
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", salt)
|
||||||
|
// We are reusing the same salt value, and that is the problem
|
||||||
|
.withUndisclosedClaim("family_name", salt)
|
||||||
|
.build()).build();
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("A salt value was reused: " + salt, exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
|
||||||
|
return IssuerSignedJwtVerificationOpts.builder()
|
||||||
|
.withVerifier(testSettings.issuerVerifierContext)
|
||||||
|
.withValidateIssuedAtClaim(false)
|
||||||
|
.withValidateExpirationClaim(false)
|
||||||
|
.withValidateNotBeforeClaim(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwt.Builder exampleFlatSdJwtV1() {
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c");
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("family_name", "Doe");
|
||||||
|
claimSet.put("email", "john.doe@example.com");
|
||||||
|
|
||||||
|
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA")
|
||||||
|
.withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ")
|
||||||
|
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return SdJwt.builder()
|
||||||
|
.withDisclosureSpec(disclosureSpec)
|
||||||
|
.withClaimSet(claimSet)
|
||||||
|
.withSigner(testSettings.issuerSigContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwt.Builder exampleFlatSdJwtV2(ObjectNode claimSet, DisclosureSpec disclosureSpec) {
|
||||||
|
return SdJwt.builder()
|
||||||
|
.withDisclosureSpec(disclosureSpec)
|
||||||
|
.withClaimSet(claimSet)
|
||||||
|
.withSigner(testSettings.issuerSigContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwt exampleAddrSdJwt() {
|
||||||
|
ObjectNode addressClaimSet = mapper.createObjectNode();
|
||||||
|
addressClaimSet.put("street_address", "Rue des Oliviers");
|
||||||
|
addressClaimSet.put("city", "Paris");
|
||||||
|
addressClaimSet.put("country", "France");
|
||||||
|
|
||||||
|
DisclosureSpec addrDisclosureSpec = DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("street_address", "AJx-095VPrpTtN4QMOqROA")
|
||||||
|
.withUndisclosedClaim("city", "G02NSrQfjFXQ7Io09syajA")
|
||||||
|
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return SdJwt.builder()
|
||||||
|
.withDisclosureSpec(addrDisclosureSpec)
|
||||||
|
.withClaimSet(addressClaimSet)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwt.Builder exampleSdJwtWithUndisclosedNestedFieldsV1() {
|
||||||
|
SdJwt addrSdJWT = exampleAddrSdJwt();
|
||||||
|
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c");
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("family_name", "Doe");
|
||||||
|
claimSet.put("email", "john.doe@example.com");
|
||||||
|
claimSet.set("address", addrSdJWT.asNestedPayload());
|
||||||
|
|
||||||
|
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA")
|
||||||
|
.withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return SdJwt.builder()
|
||||||
|
.withDisclosureSpec(disclosureSpec)
|
||||||
|
.withClaimSet(claimSet)
|
||||||
|
.withNestedSdJwt(addrSdJWT)
|
||||||
|
.withSigner(testSettings.issuerSigContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwt.Builder exampleSdJwtWithUndisclosedArrayElementsV1() throws JsonProcessingException {
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c");
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("family_name", "Doe");
|
||||||
|
claimSet.put("email", "john.doe@example.com");
|
||||||
|
claimSet.set("nationalities", mapper.readTree("[\"US\", \"DE\"]"));
|
||||||
|
|
||||||
|
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA")
|
||||||
|
.withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ")
|
||||||
|
.withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA")
|
||||||
|
.withDecoyArrayElt("nationalities", 2, "G02NSrQfjFXQ7Io09syajA")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return SdJwt.builder()
|
||||||
|
.withDisclosureSpec(disclosureSpec)
|
||||||
|
.withClaimSet(claimSet)
|
||||||
|
.withSigner(testSettings.issuerSigContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwt.Builder exampleRecursiveSdJwtV1() {
|
||||||
|
SdJwt addrSdJWT = exampleAddrSdJwt();
|
||||||
|
|
||||||
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
|
claimSet.put("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c");
|
||||||
|
claimSet.put("given_name", "John");
|
||||||
|
claimSet.put("family_name", "Doe");
|
||||||
|
claimSet.put("email", "john.doe@example.com");
|
||||||
|
claimSet.set("address", addrSdJWT.asNestedPayload());
|
||||||
|
|
||||||
|
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
|
||||||
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
|
.withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA")
|
||||||
|
.withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ")
|
||||||
|
// Making the whole address object selectively disclosable makes the process recursive
|
||||||
|
.withUndisclosedClaim("address", "BZFzhQsdPfZY1WSL-1GXKg")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return SdJwt.builder()
|
||||||
|
.withDisclosureSpec(disclosureSpec)
|
||||||
|
.withClaimSet(claimSet)
|
||||||
|
.withNestedSdJwt(addrSdJWT)
|
||||||
|
.withSigner(testSettings.issuerSigContext);
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,6 @@ import java.security.spec.ECPublicKeySpec;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.junit.ClassRule;
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.KeyUtils;
|
import org.keycloak.common.util.KeyUtils;
|
||||||
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
import org.keycloak.crypto.ECDSASignatureSignerContext;
|
||||||
|
@ -41,12 +40,12 @@ import org.keycloak.crypto.SignatureSignerContext;
|
||||||
import org.keycloak.crypto.SignatureVerifierContext;
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.keycloak.rule.CryptoInitRule;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import test-settings from:
|
* Import test-settings from:
|
||||||
* https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml
|
* <a href="https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml">
|
||||||
*
|
* open wallet foundation labs</a>
|
||||||
|
*
|
||||||
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
|
||||||
*/
|
*/
|
||||||
public class TestSettings {
|
public class TestSettings {
|
||||||
|
|
|
@ -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.sdjwtvp;
|
||||||
|
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class KeyBindingJwtVerificationOptsTest {
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void buildShouldFail_IfKeyBindingRequired_AndNonceNotSpecified() {
|
||||||
|
KeyBindingJwtVerificationOpts.builder()
|
||||||
|
.withKeyBindingRequired(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void buildShouldFail_IfKeyBindingRequired_AndNonceEmpty() {
|
||||||
|
KeyBindingJwtVerificationOpts.builder()
|
||||||
|
.withKeyBindingRequired(true)
|
||||||
|
.withNonce("")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void buildShouldFail_IfKeyBindingRequired_AndAudNotSpecified() {
|
||||||
|
KeyBindingJwtVerificationOpts.builder()
|
||||||
|
.withKeyBindingRequired(true)
|
||||||
|
.withNonce("12345678")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,442 @@
|
||||||
|
/*
|
||||||
|
* 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 com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
||||||
|
import org.keycloak.sdjwt.SdJwt;
|
||||||
|
import org.keycloak.sdjwt.TestSettings;
|
||||||
|
import org.keycloak.sdjwt.TestUtils;
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
|
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public abstract class SdJwtVPVerificationTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
// This testsuite relies on a range of test vectors (`sdjwt/s20.*-sdjwt+kb*.txt`)
|
||||||
|
// manually crafted to fit different cases. External tools were typically used,
|
||||||
|
// including mkjwk.org for generating keys, jwt.io for creating signatures, and
|
||||||
|
// base64.guru for manipulating the Base64-encoded disclosures.
|
||||||
|
|
||||||
|
static ObjectMapper mapper = new ObjectMapper();
|
||||||
|
static TestSettings testSettings = TestSettings.getInstance();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerif_s20_1_sdjwt_with_kb() throws VerificationException {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerif_s20_8_sdjwt_with_kb__AltCnfCurves() throws VerificationException {
|
||||||
|
var entries = List.of("sdjwt/s20.8-sdjwt+kb--es384.txt", "sdjwt/s20.8-sdjwt+kb--es512.txt");
|
||||||
|
|
||||||
|
for (var entry : entries) {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerif_s20_8_sdjwt_with_kb__CnfRSA() throws VerificationException {
|
||||||
|
var entries = List.of(
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt",
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt",
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt",
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var entry : entries) {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifKeyBindingNotRequired() throws VerificationException {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt");
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withKeyBindingRequired(false)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfExtraDisclosureWithNoDigest() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// One disclosure has no digest throughout Issuer-signed JWT
|
||||||
|
"sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"At least one disclosure is not protected by digest",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfFieldDisclosureLengthIncorrect() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// One field disclosure has only two elements
|
||||||
|
"sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"A field disclosure must contain exactly three elements",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfArrayElementDisclosureLengthIncorrect() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// One array element disclosure has more than two elements
|
||||||
|
"sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"An array element disclosure must contain exactly two elements",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKeyBindingRequiredAndMissing() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// This sd-jwt has no key binding jwt
|
||||||
|
"sdjwt/s6.2-presented-sdjwtvp.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withKeyBindingRequired(true)
|
||||||
|
.build(),
|
||||||
|
"Missing Key Binding JWT",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKeyBindingJwtSignatureInvalid() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// Messed up with the kb signature
|
||||||
|
"sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Key binding JWT invalid",
|
||||||
|
"VerificationException: Invalid jws signature"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfNoCnfClaim() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// This test vector has no cnf claim in Issuer-signed JWT
|
||||||
|
"sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"No cnf claim in Issuer-signed JWT for key binding",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfWrongKbTyp() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// Key Binding JWT's header: {"kid": "holder", "typ": "unexpected", "alg": "ES256"}
|
||||||
|
"sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Key Binding JWT is not of declared typ kb+jwt",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfReplayChecksFail_Nonce() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
"sdjwt/s20.1-sdjwt+kb.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withNonce("abcd") // kb's nonce is "1234567890"
|
||||||
|
.build(),
|
||||||
|
"Key binding JWT: Unexpected `nonce` value",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfReplayChecksFail_Aud() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
"sdjwt/s20.1-sdjwt+kb.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withAud("abcd") // kb's aud is "https://verifier.example.org"
|
||||||
|
.build(),
|
||||||
|
"Key binding JWT: Unexpected `aud` value",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKbSdHashWrongFormat() {
|
||||||
|
var kbPayload = exampleKbPayload();
|
||||||
|
|
||||||
|
// This hash is not a string
|
||||||
|
kbPayload.set("sd_hash", mapper.valueToTree(1234));
|
||||||
|
|
||||||
|
testShouldFailGeneric2(
|
||||||
|
kbPayload,
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Key binding JWT: Claim `sd_hash` missing or not a string",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKbSdHashInvalid() {
|
||||||
|
var kbPayload = exampleKbPayload();
|
||||||
|
|
||||||
|
// This hash makes no sense
|
||||||
|
kbPayload.put("sd_hash", "c3FmZHFmZGZlZXNkZmZi");
|
||||||
|
|
||||||
|
testShouldFailGeneric2(
|
||||||
|
kbPayload,
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Key binding JWT: Invalid `sd_hash` digest",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKbIssuedInFuture() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
var kbPayload = exampleKbPayload();
|
||||||
|
kbPayload.set("iat", mapper.valueToTree(now + 1000));
|
||||||
|
|
||||||
|
testShouldFailGeneric2(
|
||||||
|
kbPayload,
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Key binding JWT: Invalid `iat` claim",
|
||||||
|
"jwt issued in the future"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKbTooOld() {
|
||||||
|
long issuerSignedJwtIat = 1683000000; // same value in test vector
|
||||||
|
|
||||||
|
var kbPayload = exampleKbPayload();
|
||||||
|
// This KB-JWT is then issued more than 60s ago
|
||||||
|
kbPayload.set("iat", mapper.valueToTree(issuerSignedJwtIat - 120));
|
||||||
|
|
||||||
|
testShouldFailGeneric2(
|
||||||
|
kbPayload,
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withAllowedMaxAge(60)
|
||||||
|
.build(),
|
||||||
|
"Key binding JWT is too old",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKbExpired() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
var kbPayload = exampleKbPayload();
|
||||||
|
kbPayload.set("exp", mapper.valueToTree(now - 1000));
|
||||||
|
|
||||||
|
testShouldFailGeneric2(
|
||||||
|
kbPayload,
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withValidateExpirationClaim(true)
|
||||||
|
.build(),
|
||||||
|
"Key binding JWT: Invalid `exp` claim",
|
||||||
|
"jwt has expired"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfKbNotBeforeTimeYet() {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
|
var kbPayload = exampleKbPayload();
|
||||||
|
kbPayload.set("nbf", mapper.valueToTree(now + 1000));
|
||||||
|
|
||||||
|
testShouldFailGeneric2(
|
||||||
|
kbPayload,
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
.withValidateNotBeforeClaim(true)
|
||||||
|
.build(),
|
||||||
|
"Key binding JWT: Invalid `nbf` claim",
|
||||||
|
"jwt not valid yet"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfCnfNotJwk() {
|
||||||
|
// The cnf claim is not of type jwk
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt");
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
UnsupportedOperationException.class,
|
||||||
|
() -> sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("Only cnf/jwk claim supported", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfCnfJwkCantBeParsed() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// The cnf/jwk object has an unrecognized key type
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Malformed or unsupported cnf/jwk claim",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testShouldFail_IfCnfJwkCantBeParsed2() {
|
||||||
|
testShouldFailGeneric(
|
||||||
|
// HMAC cnf/jwk parsing is not supported
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt",
|
||||||
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
|
"Malformed or unsupported cnf/jwk claim",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testShouldFailGeneric(
|
||||||
|
String testFilePath,
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts,
|
||||||
|
String exceptionMessage,
|
||||||
|
String exceptionCauseMessage
|
||||||
|
) {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), testFilePath);
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
keyBindingJwtVerificationOpts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(exceptionMessage, exception.getMessage());
|
||||||
|
if (exceptionCauseMessage != null) {
|
||||||
|
assertThat(exception.getCause().getMessage(), containsString(exceptionCauseMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test helper allows replacing the key binding JWT of base
|
||||||
|
* sample `sdjwt/s20.1-sdjwt+kb.txt` to cover different scenarios.
|
||||||
|
*/
|
||||||
|
private void testShouldFailGeneric2(
|
||||||
|
JsonNode kbPayloadSubstitute,
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts,
|
||||||
|
String exceptionMessage,
|
||||||
|
String exceptionCauseMessage
|
||||||
|
) {
|
||||||
|
KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(
|
||||||
|
kbPayloadSubstitute,
|
||||||
|
testSettings.holderSigContext,
|
||||||
|
KeyBindingJWT.TYP
|
||||||
|
);
|
||||||
|
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
|
||||||
|
SdJwtVP sdJwtVP = SdJwtVP.of(
|
||||||
|
sdJwtVPString.substring(0, sdJwtVPString.lastIndexOf(SdJwt.DELIMITER) + 1)
|
||||||
|
+ keyBindingJWT.toJws()
|
||||||
|
);
|
||||||
|
|
||||||
|
var exception = assertThrows(
|
||||||
|
VerificationException.class,
|
||||||
|
() -> sdJwtVP.verify(
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
keyBindingJwtVerificationOpts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(exceptionMessage, exception.getMessage());
|
||||||
|
if (exceptionCauseMessage != null) {
|
||||||
|
assertEquals(exceptionCauseMessage, exception.getCause().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
|
||||||
|
return IssuerSignedJwtVerificationOpts.builder()
|
||||||
|
.withVerifier(testSettings.issuerVerifierContext)
|
||||||
|
.withValidateIssuedAtClaim(false)
|
||||||
|
.withValidateNotBeforeClaim(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyBindingJwtVerificationOpts.Builder defaultKeyBindingJwtVerificationOpts() {
|
||||||
|
return KeyBindingJwtVerificationOpts.builder()
|
||||||
|
.withKeyBindingRequired(true)
|
||||||
|
.withAllowedMaxAge(Integer.MAX_VALUE)
|
||||||
|
.withNonce("1234567890")
|
||||||
|
.withAud("https://verifier.example.org")
|
||||||
|
.withValidateExpirationClaim(false)
|
||||||
|
.withValidateNotBeforeClaim(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode exampleKbPayload() {
|
||||||
|
var payload = mapper.createObjectNode();
|
||||||
|
payload.put("nonce", "1234567890");
|
||||||
|
payload.put("aud", "https://verifier.example.org");
|
||||||
|
payload.put("sd_hash", "X9RrrfWt_70gHzOcovGSIt4Fms9Tf2g2hjlWVI_cxZg");
|
||||||
|
payload.set("iat", mapper.valueToTree(1702315679));
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,18 +36,15 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will try to test conformity to the spec by comparing json objects.
|
* This class will try to test conformity to the spec by comparing json objects.
|
||||||
*
|
|
||||||
*
|
|
||||||
* We are facing the situation that:
|
* We are facing the situation that:
|
||||||
* - json produced are not normalized. But we can compare them by natching their
|
* - json produced are not normalized. But we can compare them by matching their
|
||||||
* content once loaded into a json object.
|
* content once loaded into a json object.
|
||||||
* - ecdsa signature contains random component. We can't compare them directly.
|
* - ecdsa signature contains random component. We can't compare them directly.
|
||||||
* Even if we had the same input byte
|
* Even if we had the same input byte
|
||||||
* - The no rationale for ordering the disclosures. So we can only make sure
|
* - The no rationale for ordering the disclosures. So we can only make sure
|
||||||
* each of them is present and that the json content matches.
|
* each of them is present and that the json content matches.
|
||||||
*
|
* Warning: in other to produce the same disclosure strings and hashes like in
|
||||||
* Warning: in orther to produce the same disclosure strings and hashes like in
|
* the spec, i had to produce
|
||||||
* the spect, i had to produce
|
|
||||||
* the same print. This is by no way reliable enough to be used to test
|
* the same print. This is by no way reliable enough to be used to test
|
||||||
* conformity to the spec.
|
* conformity to the spec.
|
||||||
*
|
*
|
||||||
|
@ -71,8 +68,8 @@ public class TestCompareSdJwt {
|
||||||
|
|
||||||
assertEquals(e.getPayload(), a.getPayload());
|
assertEquals(e.getPayload(), a.getPayload());
|
||||||
|
|
||||||
List<String> expectedJwsStrings = Arrays.asList(e.getJwsString().split("\\."));
|
List<String> expectedJwsStrings = Arrays.asList(e.toJws().split("\\."));
|
||||||
List<String> actualJwsStrings = Arrays.asList(a.getJwsString().split("\\."));
|
List<String> actualJwsStrings = Arrays.asList(a.toJws().split("\\."));
|
||||||
|
|
||||||
// compare json content of header
|
// compare json content of header
|
||||||
assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0)));
|
assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0)));
|
||||||
|
@ -87,7 +84,7 @@ public class TestCompareSdJwt {
|
||||||
Set<JsonNode> expectedDisclosures = expectedSdJwt.getDisclosuresString().stream()
|
Set<JsonNode> expectedDisclosures = expectedSdJwt.getDisclosuresString().stream()
|
||||||
.map(TestCompareSdJwt::toJsonNode)
|
.map(TestCompareSdJwt::toJsonNode)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
Set<JsonNode> actualDisclosures = expectedSdJwt.getDisclosuresString().stream()
|
Set<JsonNode> actualDisclosures = actualSdJwt.getDisclosuresString().stream()
|
||||||
.map(TestCompareSdJwt::toJsonNode)
|
.map(TestCompareSdJwt::toJsonNode)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslB00
|
2
core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt
Normal file
2
core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,2 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiYWxnIjoiSFMyNTYifQ.qFD5kLKnWxuEwldxGxXRKfi3uuEokEBCglYKidyYHDM6mYrNIyYdjcCQaQ4Ll_KVpo7aLbzkAExxIZRtN3FwVQ
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,2 @@
|
||||||
|
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJ1bmV4cGVjdGVkIiwiYWxnIjoiRVMyNTYifQ.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.8LSkT5EJ4UTukkMeNDyo01yQn2hr2ipdCjXII4B8Jb56y1ZvqiE_r6fEUY1DoUa3tvKY21XzF0SCsUgCuY5PVg
|
|
@ -0,0 +1,5 @@
|
||||||
|
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImhlbGxvIiwgIndvcmxkIl0
|
||||||
|
~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,7 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifSx7Ii4uLiI6Im5vZkFmeDhTcWV2d3EwYWJWalJrV3BOai01NjBkU3dUUzdMbUJLR3FrZ2MifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCJ5IjoiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.sMAP6gUt1TLwdNtT-U06qbC4qZWB8i0gadzAHA5fvB-LpXTccHPZTsG9TIlgh8-vgYOnqr6t36XaHnU4217LpQ
|
||||||
|
~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiQ00iXQ
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd
|
||||||
|
~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0
|
||||||
|
~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd
|
||||||
|
~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,5 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiLCJxSjROWDR3RWk1SGl6VUg4QjZ4cGZtMmxqZkVtTzlGRF9YRmtvWFd1WFdRIl0sImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDAsInN1YiI6InVzZXJfNDIiLCJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0seyIuLi4iOiI3Q2Y2SmtQdWRyeTNsY2J3SGdlWjhraEF2MVUxT1NsZXJQMFZrQkpyV1owIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.-lSmU8_PXTnSr1wbAkoW3Xwa_VOX-dL4MlREkWjXtOHzSJ7DnDUpv_cJSh5eub3VGqxjbHnzqz0VOoLhRx47pw
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZpZWxkIl0
|
||||||
|
~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,5 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJvY3QiLCJrIjoiR0F3ZXkwSlh6U0kzTUlPLS03eUt2b2R5ZC1Yam5XR2M2OWtNT1NXZ2RoNUtQeVlPdmNCQzJNa18zZndjcEFmWGVqZEQ4TVpNUE0yY2JVdWc0RERZZGQzb1ZnVjNmYlRnRnlEdDZpYTQ3SExoeUkybFNDOXJIQ1Foa0NrczRDejNyTFBtbjhGcU1BenFFQmRxQmpmTTdxOVBvTVBvRHl3cS1iU3FpcTBnQVhrbG9nMlA2OXVpa2MxX0F3dDJRdk14ZC12SGVxVGVOb2RKVGlKUllDOTQwcW5HTXNzdlhodTVsU0tKQVNuLWRzamhaX25FQlhhbmUxZGlSZFlFY2daWDJJa196amhIa044dTBJMTNDd2Y2MS1fdHJjVFRkZG9Oal9KZkVMNGpuRHJTdVBNWFFXYzNYUFBXN193U1pGMGFEdndpWnV4YnpXVjRiVVdjS1Q1Nlh3IiwiYWxnIjoiSFMyNTYifX19.hIazN1P8S71Q0mnPaOjlN6buVyFpFlwW2B1W0RDebJdpcnb-ms8sCOx5NNi8aK_5KfCkvCECfVhNVAcQpOIyFw
|
||||||
|
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,3 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7ImtpZCI6ImRmZDFhYTk3LTZkOGQtNDU3NS1hMGZlLTM0Yjk2ZGUyYmZhZCJ9fQ.BLt9LcdgL-0HM1TV2OLLuJq9U1f8vlqha8I-WlcA-Je6e5U84HmWhYEgaBHOtt4NNrzAC-dk2xSxXjjr8aemTw
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,6 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQ1oiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.MfjyETGLaL8zJ7xYiWsfhFhvEFCA2Epj7BMsZKboOtBdHw-_ap1bjUnVY_3IDvoRLmyDzb6_AUj-OJ1IQS9_Lw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA
|
|
@ -0,0 +1,4 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTMjU2IiwibiI6ImdnUE91THM4R3U5WWVObmpfazZMQmFnOV9zNFNjdk1QRjZ4Z2t2M2ZnQnhQNVo5VWNBWUJ5Z09YczQ4UnRabzlCczhvS1NlQndXMWdHUDN2Szl1Q09iZ1cyd0JfSDQ0WG1qVk1MZnRsYnlTVzA0aHpmU3lzQWlBQ0tkdzJLZHFncEJEVDQyWHd1bEVBMV9KM3NGcWZRNkdacUwzUWRUaUpDNFZuSllSTERES0UwY2otWXlTVTZCcktfUG9WYWxqLWdOR2ZvYkRRaVJzeU0wSTVlVWpvTlM2SmxPYjlSTDlkdUh5SUdER3FrVE5kblFiaVI4Wm1NQVpyOHBPaS03WUU1dGVMVmpJUXRlOHdVcERWdk9MVXA5eVdZOE1RbjNLMUk5UTdBMGZ2bEVveTFnd1FMMkV3U29Oc1NEUDkzS1ZJdnVyYlQ1ZzVuakttZHRnZTBXSll6USJ9fX0.4LrL9rQm5GgwBT_IePfjcvwJpYgkE-s5mTUyr81kX5NblcOPdAexvojfPpnfZ2qOsv6axYkQwD3aRS5gG3oYqA
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoia2ItODdTMFlERGZUX2V5Z0pIenNyZERkZzE0dUJqXzh2MnhHa2E4WGlIMCJ9.bQXPEjBuN8I803XvX1ZK4F6FakaAb6tCo4Km5xfLXIV9ukCHySwUMRrLoP5XPVcVxBytJEJpkQ997ahs2ux3b-UN-yoBOOR6Kwc31hV7BdWU8GnSbH-6gxmB0WJPvh3fBfNfQzfTfIsTLjS9becnPoIt-1PIBQzJXGG0SHut4hjdnHEOtvnbaVwhN6Facil7A5xXoLhNsk-WBKmdBL89aFlLfpO7i1I_87uCnZXspcQ6c7kETaQReZQtJNitQrYLiFwgIv8cyiFbwPQVKwJ4XAQpt9N2I50XwTE6dhUbdAxdjRzqgoxZ-gWXMWksouyH3wrN2nKAJEs3Ya-uz1JYBQ
|
|
@ -0,0 +1,4 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTMzg0IiwibiI6ImdaMGExcTgzU24zdjhFWm84bXFYR3NhLUZGa0NNd05qWkx4SmktSU5kMnlxYnNmM1hVVThUZ2ZoVDdRS1FiVDctZmRTeWVKUkI1TTBnb3ZOOGZfdE8wQXN3aUF2M181bTRwYVBSbVhJRzNyZ0JkY2JXRnhFTDlQV0lHN3h3am13LXV0Mi1ZdnFRYkEzSTFERGdGV1Z1c0h5RzRFZUtaamVOR0hNYnhqaXRsSVZ4TV9ya09MTmVUSENDM2hJcDVzdWZ6VlJ1eXhuTFJsdXFoa09FcWdRTDJoWEdTYmx4QnJGY0h6bUU0cnIxc1c4eDlYYnR3QjhMQnJRYjBhM0gzdnpzNVJDTm5UUEh4ejhXaUx5TURlT1hjYUVTWGdnRjNmcFhLUGVOQkNfcnB5bWxIWWFRUGtuTFVRd0NiVEJESnNYYTB6b240UlVRZUpMS1hMVWJPRXU5dyJ9fX0.k3dMFI6QerNlkQV_6bEyQ94eOY6Mbu9Zk5GPdp6K96FnUk3PbKeurdxFK92lC3rXWROOSmlYXXOszagLvEnbkA
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzM4NCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiMlYtMkVqRGNfVWxLeDF1dFdpbEs1SUlMekEzOE55MU1icDBrdXN3Tm54TSJ9.dBBEuOCZOZiwWAUDc8JwzAfxsNj-ronJyykwRF0yuwNnVJoyq-t6YBTrXEKkLcN8iqu0xSvyAHS5cZIk0K49K9HoD9Qs37-KcotghULkaN-e4vaLnNe4xQOP3ujejU9Gby_QOpZ880cxzD1-6TktmpC7mIHs5laI9kJYDn56aQAZ781IPGH0YAgl7c_VSlMyt5wdAOX8xYpPwZ9HtpBEwwQ-ivw1XxngDbwnAVxXwGp0SAM8eq3z9T6L3ABoi-RSD4TtwUFvnLjPbC2-R_dZGCutGR-NVj-km88HZethOFt78KGaGGHm9Qyiw7-C23zSAniRqZM54O3JaZxmImVlEQ
|
|
@ -0,0 +1,4 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTNTEyIiwibiI6InVycnl3WlUzVTUwWWJBbDgxY2dnNnNYMkVQRk5JeFFEVkJWYU5pR2NkRVYzbGdBTVl6a3NJYi1UdGZXSVpKMG9VZnBieF9lV1Y3Znd4blZYUHBFRkdPcGNuZUlKLXBPdV9CV1dMTi1PMl9BNFRudHctUnNzNk5RTFZDdmFvSW05YlZYbDZocWNwbDI1dFNJRlExb0w4Y1hxdXUzcDQxUlZJYXVZSC1PNnRZVndQdEwxeEg5WEp2Ym1OM2ZXMjZNdEpVYVIxZWpicVp1UE9xM1hBMXkwRmU5NkJqYXdRYXRCelprekxoMDhCWHdzTkpCbFA2a2F1aTRVU3F4QkdwWVczOElQTjNQOVdSMEo4akNtVEl1d2dwQmVOUFhQSFA1U2FPdEhtUE1KeEZxNUtIY3lWN0s3OHI4LXFZRFlWSHJieGVHZ01GanFZZXRVZ3A4UHVQbGsxdyJ9fX0.LLARtgBTQPHJynt_of4J7Api8YBM_YtA8EJpF1_ZYu72BGINv5vQjPjX4ZAVzOsNZS5E4uv4RfS0q4Wxl6BNwQ
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzUxMiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiSHBaVTZDLTVSZGhEdUkwckJUTGxvQU9ZcWI4MzRScHpwYVpNQ2c3Qm03WSJ9.X5fd-bLsd_tGP83pfiS4KCCnNgO4WGfB7Sa7339RdmvbzDFPYiwFuyBq_ROAzqBU_B9NDRbRxPQGHNV_I2hYUHj-zIwIYLwS5-VkKPTWunEaL19KGLqi4uPI4ZX_1n4al5PyupDWY2EXt90Xf35KOHpVtaupYz7Z7ZWPi2uG338FD-BXiPgsBCloABdvkdq8EGx6XleBev3S43cW33f-Zozw75L1-WgF_cnObVnFT_7_nOk4N8InGU46SyL_CyeCo-_LXdKN_tDZ2Mi6AEBKwJoD3WY6sf_uI49d1o1USs4AR9PcedbwQKDV-RzF_XQRqD6TZfOEvT6KJtyVUOU2DQ
|
|
@ -0,0 +1,4 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsIm4iOiJpWGtzVXQ5ZXY2NFdRdmdmM3llM0VTWXZFcGxkWkNkNGlBNUgwbVBzLUkyRjNmUjNLd1E2M0dWd2FuTDdHNlk3Z1REZ2YyZzE3SjhpVHVic210WGttUnFHdzNNb0JoLVVLYncwYjUyZTZtZm0wbG5wT24yMElXamxKcXpaVDROUTZ2eFpqMkdXbEx0bUhvUTBpM0JTQTliMW5CVDBkMHVNYk9kZHlRS0plOEtRTGJIZUoyOXF3UEFzQWl5X3R5czBkd3d2R1dmV2VKUHVvTTNfY2pxcTRXQ0Z3ZDllNXlXejU2VFFXZFljSDZ3dFB6RzM4R0JPc3hOdmtTSW53NXhyWXZlOHZzcjNLY0tISjYySDE2NHhZU2ZNQ3BiYXVhSkFTY3hrcnUwRUwwUXVxVzB6dzZKdjRiZVZITUtqSXo1NWxHYTV4QmFybWxxSkZBRnl5MWwwclEifX19.WMEOfYaPIQFTY79rNRHeqoz4eTDyrhXJOtm_zncW5aJ10vTzSA7tsVvREzn6fajP21EI-ZNWuTzD3Ji88OI6SA
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJSUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiRWRER05ldkJ5YlVCMnpSYnpBZXl1Z2hHa2l3UG5tZVRCQS10QlhGVHpocyJ9.Bhec_71SLidxezU4HqkvrtWPF16pHkrPos3OL9y1rOR0ACgZ2KEigFr7pIn59_be60xi-EeNvAo1zt0N5uILBd3jkKjRmpC2MO2ZkIgKterJN_MEcCXlOQZc48QoDJIBuvmXq5wmIZMVfUJTw9i2PhtfaX49K5Fpmf3s9Iv4WnJLY7wVswiIYNFckKxal9agTCKNxZ5SAyz_3mZ3VJYeSG7d9IjhQ3w7w19jcsdaL635qt_Vf75dDodLZjlh1N0VhRqxbQj2sl4NbrC3Ezr7JXcSdUipn5vjRSgV4g8-ws-EF2NMhwPR4Ut_HSXNpge2NMqJcaTjXnmmX6RQesdGyA
|
5
core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt
Normal file
5
core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJDNnJDUnJSUmM3Z3ZtUFU4S3pOVTEweXRIRW1oRVFhaTY5SkxBeDU3U0FrcHlsNlpxazVXV2FlcUFUNVdDajV0IiwiY3J2IjoiUC0zODQiLCJ4IjoiS2EyazVKRjBSZkVQMFlVU2lFODNmZ1VVS3VIRC16bWQtdXlkYXJMN1JKVjFtdGd3MkhjNU80d0ZJQm85Zk9KOSIsInkiOiJFdEIwSGV1dTlubmZjcDlCLXdGaUdWN3dCT1plTUpMTGVPaHpfUFRiUUxhdUgyTEcwQ25fRlFYajJRZURGOGwxIn19fQ.udOcVKk1WTxg5XldomVczJY2Dptiz4sFf8OQADUC0PaYzOwIl5CjMuTHhs1K-tORGfIO7nPAe_VCLC0jXaSzgQ
|
||||||
|
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzM4NCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiaFN3cXVsbDJ4VWMtNDQwdkhLX0RUdnVXZWxDUDBlSVo0d0JOcXNOeVAySSJ9.TA93w_A3IBornn6Gu81oNjT2M-evVz6_TyCWTX-ZfL9uXkeiP44hRn0irCwCy0krtHrq49EyZxXLM2o9qRGYw1cDi68u2gYMEHLiXZzXu51q0ckQ2pjsTYDE2pqrSOZT
|
4
core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt
Normal file
4
core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJBQm0xYmVXQ2xTS2ZwRTBwYk94aGVqd21XekhjdmVrMGo3N3RrWWkxYUZtclktLUtxcWxDV3FlQnZpcUwzaU5ZcnFLTEx4ZGxOOS1nNXlHZjZjUHl1MUVIIiwiY3J2IjoiUC01MjEiLCJ4IjoiQU1uY3B1bTBmZ240V2hfZUswbTFhNWdzX2MyelpVb2hGLUlvQTZ1OUhGejlyZVlxX3c4d1VMZFZaNXdySHp2MGFyOG94MmRXZWp1WDIza0FLVm8wdUZJRSIsInkiOiJBYldlOVBOd0VFUlNrU0pZRXJBektNeWNVczAwLXBlZVl2MlVFd1FYZlM5ZFZ5ZVJGMGxiU2E5WlYtZzVlWGJuRXpuUk5sa2xLcHVaeTdncVppUENPRGkyIn19fQ.QoxsCI_hP2bazbr9sS2uE93vQ1DhD8Qdrjg0csou00I8XVKbmccLlHuKHALGYEqhFWVIQ5pCSL2XCkxnz-t5uQ
|
||||||
|
|
||||||
|
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzUxMiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWmM4aGNvVlBvN0VQTktQZzRjd05VdTFXMGtieWFwOC1zck5xWEpyUkhzOCJ9.APIud1JXrH0BLSD3TLoLQvkGS-48zqYcaB6sANnxXRDMlzHiqdqr_FnGD0QcY_VJcD_8EMhUvlrGty0qfSWMPDkHADyZIQIPTsz-5lCbPV6WU5IILprmov_PloxC-JNz58lo7Ak5hbnqJ2wZ6UAqN98XV2DMgIv84UcyezXLy23uszWm
|
|
@ -7,6 +7,7 @@
|
||||||
"key_size": 256,
|
"key_size": 256,
|
||||||
"kty": "EC",
|
"kty": "EC",
|
||||||
"issuer_key": {
|
"issuer_key": {
|
||||||
|
"kid": "doc-signer-05-25-2022",
|
||||||
"kty": "EC",
|
"kty": "EC",
|
||||||
"d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g",
|
"d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g",
|
||||||
"crv": "P-256",
|
"crv": "P-256",
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
"y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8"
|
"y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8"
|
||||||
},
|
},
|
||||||
"holder_key": {
|
"holder_key": {
|
||||||
|
"kid": "holder",
|
||||||
"kty": "EC",
|
"kty": "EC",
|
||||||
"d": "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I",
|
"d": "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I",
|
||||||
"crv": "P-256",
|
"crv": "P-256",
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.crypto.def.test.sdjwt;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.sdjwt.SdJwsTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class DefaultCryptoSdJwsTest extends SdJwsTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
// Run this test just if java is not in FIPS mode
|
||||||
|
Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.crypto.def.test;
|
package org.keycloak.crypto.def.test.sdjwt;
|
||||||
|
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.crypto.def.test.sdjwt;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.sdjwt.sdjwtvp.SdJwtVPVerificationTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class DefaultCryptoSdJwtVPVerificationTest extends SdJwtVPVerificationTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
// Run this test just if java is not in FIPS mode
|
||||||
|
Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.crypto.def.test.sdjwt;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.sdjwt.SdJwtVerificationTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class DefaultCryptoSdJwtVerificationTest extends SdJwtVerificationTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
// Run this test just if java is not in FIPS mode
|
||||||
|
Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,8 +30,11 @@ import java.security.cert.CertStore;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.CollectionCertStoreParameters;
|
import java.security.cert.CollectionCertStoreParameters;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
import java.security.spec.ECGenParameterSpec;
|
import java.security.spec.ECGenParameterSpec;
|
||||||
import java.security.spec.ECParameterSpec;
|
import java.security.spec.ECParameterSpec;
|
||||||
|
import java.security.spec.MGF1ParameterSpec;
|
||||||
|
import java.security.spec.PSSParameterSpec;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@ -140,7 +143,7 @@ public class WildFlyElytronProvider implements CryptoProvider {
|
||||||
public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException {
|
public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException {
|
||||||
return SecretKeyFactory.getInstance(keyAlgorithm);
|
return SecretKeyFactory.getInstance(keyAlgorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException {
|
public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException {
|
||||||
return KeyStore.getInstance(format.toString());
|
return KeyStore.getInstance(format.toString());
|
||||||
|
@ -165,8 +168,28 @@ public class WildFlyElytronProvider implements CryptoProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException {
|
public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException {
|
||||||
return Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(sigAlgName));
|
String javaAlgorithm = JavaAlgorithm.getJavaAlgorithm(sigAlgName);
|
||||||
|
|
||||||
|
switch (javaAlgorithm) {
|
||||||
|
case JavaAlgorithm.PS256, JavaAlgorithm.PS384, JavaAlgorithm.PS512:
|
||||||
|
var signature = Signature.getInstance("RSASSA-PSS");
|
||||||
|
|
||||||
|
int digestLength = Integer.parseInt(javaAlgorithm.substring(3, 6));
|
||||||
|
MGF1ParameterSpec ps = new MGF1ParameterSpec("SHA-" + digestLength);
|
||||||
|
AlgorithmParameterSpec params = new PSSParameterSpec(
|
||||||
|
ps.getDigestAlgorithm(), "MGF1", ps, digestLength / 8, 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
signature.setParameter(params);
|
||||||
|
} catch (InvalidAlgorithmParameterException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Signature.getInstance(javaAlgorithm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.crypto.elytron.test.sdjwt;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.sdjwt.SdJwsTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class ElytronCryptoSdJwsTest extends SdJwsTest {
|
||||||
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.crypto.elytron.test;
|
package org.keycloak.crypto.elytron.test.sdjwt;
|
||||||
|
|
||||||
import org.keycloak.sdjwt.sdjwtvp.SdJwtVPTest;
|
import org.keycloak.sdjwt.sdjwtvp.SdJwtVPTest;
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.crypto.elytron.test.sdjwt;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.sdjwt.sdjwtvp.SdJwtVPVerificationTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class ElytronCryptoSdJwtVPVerificationTest extends SdJwtVPVerificationTest {
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.crypto.elytron.test.sdjwt;
|
||||||
|
|
||||||
|
import org.keycloak.sdjwt.SdJwtVerificationTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class ElytronCryptoSdJwtVerificationTest extends SdJwtVerificationTest {
|
||||||
|
}
|
Loading…
Reference in a new issue