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:
Ingrid Kamga 2024-08-05 10:50:03 +01:00 committed by GitHub
parent 4080ee2e84
commit 36a141007e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2620 additions and 44 deletions

View file

@ -17,7 +17,10 @@
package org.keycloak.crypto;
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.Signature;
@ -42,7 +45,7 @@ public class AsymmetricSignatureVerifierContext implements SignatureVerifierCont
@Override
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
try {
Signature verifier = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(key.getAlgorithmOrDefault(), key.getCurve()));
Signature verifier = getSignature();
verifier.initVerify((PublicKey) key.getPublicKey());
verifier.update(data);
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());
}
}
}

View file

@ -30,7 +30,7 @@ public class DisclosureRedList {
private final Set<SdJwtClaimName> redListClaimNames;
public static final DisclosureRedList defaultList = defaultList();
public DisclosureRedList of(Set<SdJwtClaimName> redListClaimNames) {
public static DisclosureRedList of(Set<SdJwtClaimName> redListClaimNames) {
return new DisclosureRedList(redListClaimNames);
}

View file

@ -20,8 +20,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.JWSInput;
@ -39,13 +42,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
*/
public class IssuerSignedJWT extends SdJws {
public static IssuerSignedJWT fromJws(String jwsString) {
return new IssuerSignedJWT(jwsString);
public IssuerSignedJWT(JsonNode payload, SignatureSignerContext signer, String jwsType) {
super(payload, signer, jwsType);
}
public IssuerSignedJWT toSignedJWT(SignatureSignerContext signer, String jwsType) {
JWSInput jwsInput = sign(getPayload(), signer, jwsType);
return new IssuerSignedJWT(getPayload(), jwsInput);
public static IssuerSignedJWT fromJws(String jwsString) {
return new IssuerSignedJWT(jwsString);
}
private IssuerSignedJWT(String jwsString) {
@ -134,6 +136,43 @@ public class IssuerSignedJWT extends SdJws {
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
public static final String CLAIM_NAME_SELECTIVE_DISCLOSURE = "_sd";
public static final String CLAIM_NAME_SD_HASH_ALGORITHM = "_sd_alg";

View file

@ -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
);
}
}
}

View file

@ -17,12 +17,15 @@
package org.keycloak.sdjwt;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
@ -34,7 +37,7 @@ import com.fasterxml.jackson.databind.JsonNode;
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*
*/
public class SdJws {
public abstract class SdJws {
private final JWSInput jwsInput;
private final JsonNode payload;
@ -49,10 +52,6 @@ public class SdJws {
return payload;
}
public String getJwsString() {
return jwsInput.getWireString();
}
// Constructor for unsigned JWS
protected SdJws(JsonNode payload) {
this.payload = payload;
@ -107,4 +106,75 @@ public class SdJws {
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));
}
}
}

View file

@ -24,6 +24,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.sdjwt.vp.KeyBindingJWT;
@ -194,6 +195,19 @@ public class SdJwt {
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
public static class Builder {
private DisclosureSpec disclosureSpec;

View file

@ -20,14 +20,17 @@ import java.io.IOException;
import java.security.SecureRandom;
import java.util.Optional;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Base64Url;
import org.keycloak.jose.jws.crypto.HashUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.node.ArrayNode;
/**
*
@ -36,12 +39,16 @@ import com.fasterxml.jackson.databind.ObjectWriter;
public class SdJwtUtils {
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) {
return Base64Url.encode(bytes);
}
public static byte[] decodeNoPad(String encoded) {
return Base64Url.decode(encoded);
}
public static String hashAndBase64EncodeNoPad(byte[] disclosureBytes, String hashAlg) {
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 class ArraySpacedPrettyPrinter extends MinimalPrettyPrinter {

View file

@ -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;
}
}
}

View file

@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode;
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayElement {
public static final String SD_CLAIM_NAME = "...";
private final JsonNode arrayElement;
private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) {
@ -34,7 +35,7 @@ public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayEl
@Override
public JsonNode getVisibleValue(String hashAlg) {
return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg));
return SdJwtUtils.mapper.createObjectNode().put(SD_CLAIM_NAME, getDisclosureDigest(hashAlg));
}
@Override

View file

@ -29,13 +29,18 @@ import com.fasterxml.jackson.databind.JsonNode;
*/
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) {
return new KeyBindingJWT(jwsString);
}
public static KeyBindingJWT from(JsonNode payload, SignatureSignerContext signer, String jwsType) {
JWSInput jwsInput = sign(payload, signer, jwsType);
return new KeyBindingJWT(payload, jwsInput);
return new KeyBindingJWT(payload, signer, jwsType);
}
private KeyBindingJWT(JsonNode payload, JWSInput jwsInput) {

View file

@ -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
);
}
}
}

View file

@ -27,11 +27,14 @@ import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Base64Url;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.sdjwt.IssuerSignedJWT;
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
import org.keycloak.sdjwt.SdJwt;
import org.keycloak.sdjwt.SdJwtUtils;
import org.keycloak.sdjwt.SdJwtVerificationContext;
import com.fasterxml.jackson.databind.JsonNode;
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>
*/
public class SdJwtVP {
private String sdJwtVpString;
private final String sdJwtVpString;
private final IssuerSignedJWT issuerSignedJWT;
private final Map<String, ArrayNode> claims;
@ -52,6 +55,10 @@ public class SdJwtVP {
private final Optional<KeyBindingJWT> keyBindingJWT;
public Map<String, ArrayNode> getClaims() {
return claims;
}
public IssuerSignedJWT getIssuerSignedJWT() {
return issuerSignedJWT;
}
@ -171,7 +178,7 @@ public class SdJwtVP {
}
public JsonNode getCnfClaim() {
return issuerSignedJWT.getPayload().get("cnf");
return issuerSignedJWT.getCnfClaim().orElse(null);
}
public String present(List<String> disclosureDigests, JsonNode keyBindingClaims,
@ -195,11 +202,31 @@ public class SdJwtVP {
String sd_hash = SdJwtUtils.hashAndBase64EncodeNoPad(unboundPresentation.getBytes(), getHashAlgorithm());
keyBindingClaims = ((ObjectNode) keyBindingClaims).put("sd_hash", sd_hash);
KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(keyBindingClaims, holdSignatureSignerContext, jwsType);
sb.append(keyBindingJWT.getJwsString());
sb.append(keyBindingJWT.toJws());
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.
private static JsonNode findNode(JsonNode node, String value) {
if (node == null) {

View 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) {
};
}
}

View 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);
}
}

View file

@ -30,7 +30,6 @@ import java.security.spec.ECPublicKeySpec;
import java.util.HashMap;
import java.util.Map;
import org.junit.ClassRule;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.crypto.ECDSASignatureSignerContext;
@ -41,11 +40,11 @@ import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import com.fasterxml.jackson.databind.JsonNode;
import org.keycloak.rule.CryptoInitRule;
/**
* 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>
*/

View file

@ -0,0 +1,48 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt.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();
}
}

View file

@ -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;
}
}

View file

@ -36,18 +36,15 @@ import com.fasterxml.jackson.databind.JsonNode;
/**
* This class will try to test conformity to the spec by comparing json objects.
*
*
* We are facing the situation that:
* - json produced are not normalized. But we can compare them by natching their
* - json produced are not normalized. But we can compare them by matching their
* content once loaded into a json object.
* - ecdsa signature contains random component. We can't compare them directly.
* Even if we had the same input byte
* - The no rationale for ordering the disclosures. So we can only make sure
* each of them is present and that the json content matches.
*
* Warning: in orther to produce the same disclosure strings and hashes like in
* the spect, i had to produce
* Warning: in other to produce the same disclosure strings and hashes like in
* the spec, i had to produce
* the same print. This is by no way reliable enough to be used to test
* conformity to the spec.
*
@ -71,8 +68,8 @@ public class TestCompareSdJwt {
assertEquals(e.getPayload(), a.getPayload());
List<String> expectedJwsStrings = Arrays.asList(e.getJwsString().split("\\."));
List<String> actualJwsStrings = Arrays.asList(a.getJwsString().split("\\."));
List<String> expectedJwsStrings = Arrays.asList(e.toJws().split("\\."));
List<String> actualJwsStrings = Arrays.asList(a.toJws().split("\\."));
// compare json content of header
assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0)));
@ -87,7 +84,7 @@ public class TestCompareSdJwt {
Set<JsonNode> expectedDisclosures = expectedSdJwt.getDisclosuresString().stream()
.map(TestCompareSdJwt::toJsonNode)
.collect(Collectors.toSet());
Set<JsonNode> actualDisclosures = expectedSdJwt.getDisclosuresString().stream()
Set<JsonNode> actualDisclosures = actualSdJwt.getDisclosuresString().stream()
.map(TestCompareSdJwt::toJsonNode)
.collect(Collectors.toSet());

View file

@ -0,0 +1,2 @@
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslB00

View file

@ -0,0 +1,2 @@
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,2 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiYWxnIjoiSFMyNTYifQ.qFD5kLKnWxuEwldxGxXRKfi3uuEokEBCglYKidyYHDM6mYrNIyYdjcCQaQ4Ll_KVpo7aLbzkAExxIZRtN3FwVQ
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,2 @@
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJ1bmV4cGVjdGVkIiwiYWxnIjoiRVMyNTYifQ.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.8LSkT5EJ4UTukkMeNDyo01yQn2hr2ipdCjXII4B8Jb56y1ZvqiE_r6fEUY1DoUa3tvKY21XzF0SCsUgCuY5PVg

View file

@ -0,0 +1,5 @@
eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImhlbGxvIiwgIndvcmxkIl0
~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,7 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifSx7Ii4uLiI6Im5vZkFmeDhTcWV2d3EwYWJWalJrV3BOai01NjBkU3dUUzdMbUJLR3FrZ2MifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCJ5IjoiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.sMAP6gUt1TLwdNtT-U06qbC4qZWB8i0gadzAHA5fvB-LpXTccHPZTsG9TIlgh8-vgYOnqr6t36XaHnU4217LpQ
~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiQ00iXQ
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd
~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0
~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd
~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,5 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiLCJxSjROWDR3RWk1SGl6VUg4QjZ4cGZtMmxqZkVtTzlGRF9YRmtvWFd1WFdRIl0sImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDAsInN1YiI6InVzZXJfNDIiLCJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0seyIuLi4iOiI3Q2Y2SmtQdWRyeTNsY2J3SGdlWjhraEF2MVUxT1NsZXJQMFZrQkpyV1owIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.-lSmU8_PXTnSr1wbAkoW3Xwa_VOX-dL4MlREkWjXtOHzSJ7DnDUpv_cJSh5eub3VGqxjbHnzqz0VOoLhRx47pw
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZpZWxkIl0
~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,5 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJvY3QiLCJrIjoiR0F3ZXkwSlh6U0kzTUlPLS03eUt2b2R5ZC1Yam5XR2M2OWtNT1NXZ2RoNUtQeVlPdmNCQzJNa18zZndjcEFmWGVqZEQ4TVpNUE0yY2JVdWc0RERZZGQzb1ZnVjNmYlRnRnlEdDZpYTQ3SExoeUkybFNDOXJIQ1Foa0NrczRDejNyTFBtbjhGcU1BenFFQmRxQmpmTTdxOVBvTVBvRHl3cS1iU3FpcTBnQVhrbG9nMlA2OXVpa2MxX0F3dDJRdk14ZC12SGVxVGVOb2RKVGlKUllDOTQwcW5HTXNzdlhodTVsU0tKQVNuLWRzamhaX25FQlhhbmUxZGlSZFlFY2daWDJJa196amhIa044dTBJMTNDd2Y2MS1fdHJjVFRkZG9Oal9KZkVMNGpuRHJTdVBNWFFXYzNYUFBXN193U1pGMGFEdndpWnV4YnpXVjRiVVdjS1Q1Nlh3IiwiYWxnIjoiSFMyNTYifX19.hIazN1P8S71Q0mnPaOjlN6buVyFpFlwW2B1W0RDebJdpcnb-ms8sCOx5NNi8aK_5KfCkvCECfVhNVAcQpOIyFw
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,3 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7ImtpZCI6ImRmZDFhYTk3LTZkOGQtNDU3NS1hMGZlLTM0Yjk2ZGUyYmZhZCJ9fQ.BLt9LcdgL-0HM1TV2OLLuJq9U1f8vlqha8I-WlcA-Je6e5U84HmWhYEgaBHOtt4NNrzAC-dk2xSxXjjr8aemTw
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,6 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQ1oiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.MfjyETGLaL8zJ7xYiWsfhFhvEFCA2Epj7BMsZKboOtBdHw-_ap1bjUnVY_3IDvoRLmyDzb6_AUj-OJ1IQS9_Lw
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA

View file

@ -0,0 +1,4 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTMjU2IiwibiI6ImdnUE91THM4R3U5WWVObmpfazZMQmFnOV9zNFNjdk1QRjZ4Z2t2M2ZnQnhQNVo5VWNBWUJ5Z09YczQ4UnRabzlCczhvS1NlQndXMWdHUDN2Szl1Q09iZ1cyd0JfSDQ0WG1qVk1MZnRsYnlTVzA0aHpmU3lzQWlBQ0tkdzJLZHFncEJEVDQyWHd1bEVBMV9KM3NGcWZRNkdacUwzUWRUaUpDNFZuSllSTERES0UwY2otWXlTVTZCcktfUG9WYWxqLWdOR2ZvYkRRaVJzeU0wSTVlVWpvTlM2SmxPYjlSTDlkdUh5SUdER3FrVE5kblFiaVI4Wm1NQVpyOHBPaS03WUU1dGVMVmpJUXRlOHdVcERWdk9MVXA5eVdZOE1RbjNLMUk5UTdBMGZ2bEVveTFnd1FMMkV3U29Oc1NEUDkzS1ZJdnVyYlQ1ZzVuakttZHRnZTBXSll6USJ9fX0.4LrL9rQm5GgwBT_IePfjcvwJpYgkE-s5mTUyr81kX5NblcOPdAexvojfPpnfZ2qOsv6axYkQwD3aRS5gG3oYqA
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoia2ItODdTMFlERGZUX2V5Z0pIenNyZERkZzE0dUJqXzh2MnhHa2E4WGlIMCJ9.bQXPEjBuN8I803XvX1ZK4F6FakaAb6tCo4Km5xfLXIV9ukCHySwUMRrLoP5XPVcVxBytJEJpkQ997ahs2ux3b-UN-yoBOOR6Kwc31hV7BdWU8GnSbH-6gxmB0WJPvh3fBfNfQzfTfIsTLjS9becnPoIt-1PIBQzJXGG0SHut4hjdnHEOtvnbaVwhN6Facil7A5xXoLhNsk-WBKmdBL89aFlLfpO7i1I_87uCnZXspcQ6c7kETaQReZQtJNitQrYLiFwgIv8cyiFbwPQVKwJ4XAQpt9N2I50XwTE6dhUbdAxdjRzqgoxZ-gWXMWksouyH3wrN2nKAJEs3Ya-uz1JYBQ

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJDNnJDUnJSUmM3Z3ZtUFU4S3pOVTEweXRIRW1oRVFhaTY5SkxBeDU3U0FrcHlsNlpxazVXV2FlcUFUNVdDajV0IiwiY3J2IjoiUC0zODQiLCJ4IjoiS2EyazVKRjBSZkVQMFlVU2lFODNmZ1VVS3VIRC16bWQtdXlkYXJMN1JKVjFtdGd3MkhjNU80d0ZJQm85Zk9KOSIsInkiOiJFdEIwSGV1dTlubmZjcDlCLXdGaUdWN3dCT1plTUpMTGVPaHpfUFRiUUxhdUgyTEcwQ25fRlFYajJRZURGOGwxIn19fQ.udOcVKk1WTxg5XldomVczJY2Dptiz4sFf8OQADUC0PaYzOwIl5CjMuTHhs1K-tORGfIO7nPAe_VCLC0jXaSzgQ
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzM4NCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiaFN3cXVsbDJ4VWMtNDQwdkhLX0RUdnVXZWxDUDBlSVo0d0JOcXNOeVAySSJ9.TA93w_A3IBornn6Gu81oNjT2M-evVz6_TyCWTX-ZfL9uXkeiP44hRn0irCwCy0krtHrq49EyZxXLM2o9qRGYw1cDi68u2gYMEHLiXZzXu51q0ckQ2pjsTYDE2pqrSOZT

View file

@ -0,0 +1,4 @@
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJBQm0xYmVXQ2xTS2ZwRTBwYk94aGVqd21XekhjdmVrMGo3N3RrWWkxYUZtclktLUtxcWxDV3FlQnZpcUwzaU5ZcnFLTEx4ZGxOOS1nNXlHZjZjUHl1MUVIIiwiY3J2IjoiUC01MjEiLCJ4IjoiQU1uY3B1bTBmZ240V2hfZUswbTFhNWdzX2MyelpVb2hGLUlvQTZ1OUhGejlyZVlxX3c4d1VMZFZaNXdySHp2MGFyOG94MmRXZWp1WDIza0FLVm8wdUZJRSIsInkiOiJBYldlOVBOd0VFUlNrU0pZRXJBektNeWNVczAwLXBlZVl2MlVFd1FYZlM5ZFZ5ZVJGMGxiU2E5WlYtZzVlWGJuRXpuUk5sa2xLcHVaeTdncVppUENPRGkyIn19fQ.QoxsCI_hP2bazbr9sS2uE93vQ1DhD8Qdrjg0csou00I8XVKbmccLlHuKHALGYEqhFWVIQ5pCSL2XCkxnz-t5uQ
~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzUxMiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWmM4aGNvVlBvN0VQTktQZzRjd05VdTFXMGtieWFwOC1zck5xWEpyUkhzOCJ9.APIud1JXrH0BLSD3TLoLQvkGS-48zqYcaB6sANnxXRDMlzHiqdqr_FnGD0QcY_VJcD_8EMhUvlrGty0qfSWMPDkHADyZIQIPTsz-5lCbPV6WU5IILprmov_PloxC-JNz58lo7Ak5hbnqJ2wZ6UAqN98XV2DMgIv84UcyezXLy23uszWm

View file

@ -7,6 +7,7 @@
"key_size": 256,
"kty": "EC",
"issuer_key": {
"kid": "doc-signer-05-25-2022",
"kty": "EC",
"d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g",
"crv": "P-256",
@ -14,6 +15,7 @@
"y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8"
},
"holder_key": {
"kid": "holder",
"kty": "EC",
"d": "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I",
"crv": "P-256",

View file

@ -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());
}
}

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.crypto.def.test;
package org.keycloak.crypto.def.test.sdjwt;
import org.junit.Assume;
import org.junit.Before;

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -30,8 +30,11 @@ import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -165,8 +168,28 @@ public class WildFlyElytronProvider implements CryptoProvider {
@Override
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

View file

@ -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 {
}

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.crypto.elytron.test;
package org.keycloak.crypto.elytron.test.sdjwt;
import org.keycloak.sdjwt.sdjwtvp.SdJwtVPTest;

View file

@ -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 {
}

View file

@ -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 {
}