diff --git a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java index 76f1733a4b16..202d19d9914b 100644 --- a/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java +++ b/core/src/main/java/org/keycloak/crypto/AsymmetricSignatureVerifierContext.java @@ -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 String getAlgorithm() { @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 boolean verify(byte[] data, byte[] signature) throws VerificationExceptio } } + 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()); + } + } } diff --git a/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java b/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java index 9bb91968850d..ca30c47f3026 100644 --- a/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java +++ b/core/src/main/java/org/keycloak/sdjwt/DisclosureRedList.java @@ -30,7 +30,7 @@ public class DisclosureRedList { private final Set redListClaimNames; public static final DisclosureRedList defaultList = defaultList(); - public DisclosureRedList of(Set redListClaimNames) { + public static DisclosureRedList of(Set redListClaimNames) { return new DisclosureRedList(redListClaimNames); } diff --git a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java index 694969389db2..ff31d4d6932c 100644 --- a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java +++ b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJWT.java @@ -20,8 +20,11 @@ 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 @@ */ 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 @@ private static JsonNode generatePayloadString(List claims, List 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 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"; diff --git a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java new file mode 100644 index 000000000000..1f189a117b2e --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java @@ -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 Ingrid Kamga + */ +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 + ); + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJws.java b/core/src/main/java/org/keycloak/sdjwt/SdJws.java index 68d57a94c7cc..168597e46fd3 100644 --- a/core/src/main/java/org/keycloak/sdjwt/SdJws.java +++ b/core/src/main/java/org/keycloak/sdjwt/SdJws.java @@ -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; @@ -30,11 +33,11 @@ /** * Handle jws, either the issuer jwt or the holder key binding jwt. - * + * * @author Francis Pouatcha - * + * */ -public class SdJws { +public abstract class SdJws { private final JWSInput jwsInput; private final JsonNode payload; @@ -49,10 +52,6 @@ public JsonNode getPayload() { return payload; } - public String getJwsString() { - return jwsInput.getWireString(); - } - // Constructor for unsigned JWS protected SdJws(JsonNode payload) { this.payload = payload; @@ -107,4 +106,75 @@ private static final JsonNode readPayload(JWSInput jwsInput) { 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 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 vcts) throws VerificationException { + verifyClaimAgainstTrustedValues(vcts, "vct"); + } + + private void verifyClaimAgainstTrustedValues(List 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)); + } + } } diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwt.java b/core/src/main/java/org/keycloak/sdjwt/SdJwt.java index dbf615ce286d..e2726d8431eb 100644 --- a/core/src/main/java/org/keycloak/sdjwt/SdJwt.java +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwt.java @@ -24,6 +24,7 @@ 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 List getDisclosures() { 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; diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java index 56c866b64948..a6ef79f9f03e 100644 --- a/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtUtils.java @@ -20,28 +20,35 @@ 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; /** - * + * * @author Francis Pouatcha */ 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 static String printJsonArray(Object[] array) throws JsonProcessingExcepti } } + 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 { diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java new file mode 100644 index 000000000000..cce22c6b4722 --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java @@ -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 Ingrid Kamga + */ +public class SdJwtVerificationContext { + private String sdJwtVpString; + + private final IssuerSignedJWT issuerSignedJwt; + private final Map disclosures; + private KeyBindingJWT keyBindingJwt; + + public SdJwtVerificationContext( + String sdJwtVpString, + IssuerSignedJWT issuerSignedJwt, + Map disclosures, + KeyBindingJWT keyBindingJwt) { + this(issuerSignedJwt, disclosures); + this.keyBindingJwt = keyBindingJwt; + this.sdJwtVpString = sdJwtVpString; + } + + public SdJwtVerificationContext(IssuerSignedJWT issuerSignedJwt, Map disclosures) { + this.issuerSignedJwt = issuerSignedJwt; + this.disclosures = disclosures; + } + + public SdJwtVerificationContext(IssuerSignedJWT issuerSignedJwt, List disclosureStrings) { + this.issuerSignedJwt = issuerSignedJwt; + this.disclosures = computeDigestDisclosureMap(disclosureStrings); + } + + private Map computeDigestDisclosureMap(List 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. + * + *

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, 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. + * + *

+ * 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. + *

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

+ * 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 + *

+ * + * @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. + * + *

+ * 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. + *

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

+ * 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) + *

+ * + *

+ * We additionally check that salt values are not reused: + * The salt value MUST be unique for each claim that is to be selectively disclosed. + *

+ * + * @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 visitedSalts = new HashSet<>(); + Set visitedDigests = new HashSet<>(); + Set 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. + * + *

+ * 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. + *

+ * + * @return the fully disclosed SdJwt payload + */ + private JsonNode validateViaRecursiveDisclosing( + JsonNode currentNode, + Set visitedSalts, + Set visitedDigests, + Set 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(); + + 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. + * + *

+ * 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. + *

+ * + * @throws VerificationException if not first visit + */ + private void markDigestAsVisited(String digest, Set 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. + * + *

+ * The salt value MUST be unique for each claim that is to be selectively disclosed. + *

+ * + * @throws VerificationException if not first visit + */ + private void markSaltAsVisited(String salt, Set 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. + * + *

+ * 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. + *

+ * + *

+ * If the claim name is _sd or ..., the SD-JWT MUST be rejected. + *

+ * + * @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. + * + *

+ * If the contents of the respective Disclosure is not a JSON-encoded array of + * two elements (salt, value), the SD-JWT MUST be rejected. + *

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

+ * 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. + *

+ * + * @throws VerificationException if not the case + */ + private void validateDisclosuresVisits(Set 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. + * + *

+ * 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. + *

+ * + * @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. + * + *

+ * 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. + *

+ * + * @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; + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java b/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java index 140938e55636..da6aaf131470 100644 --- a/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java +++ b/core/src/main/java/org/keycloak/sdjwt/UndisclosedArrayElement.java @@ -25,6 +25,7 @@ * @author Francis Pouatcha */ 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 @@ private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) { @Override public JsonNode getVisibleValue(String hashAlg) { - return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg)); + return SdJwtUtils.mapper.createObjectNode().put(SD_CLAIM_NAME, getDisclosureDigest(hashAlg)); } @Override diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java index d9e0ab94be2a..9c70d17bf0c4 100644 --- a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java +++ b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJWT.java @@ -23,19 +23,24 @@ import com.fasterxml.jackson.databind.JsonNode; /** - * + * * @author Francis Pouatcha - * + * */ 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) { diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java new file mode 100644 index 000000000000..6f43f91a38ba --- /dev/null +++ b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java @@ -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 Ingrid Kamga + */ +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 + ); + } + } +} diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java b/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java index 8702f4eae9a2..7c693a7f358b 100644 --- a/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java +++ b/core/src/main/java/org/keycloak/sdjwt/vp/SdJwtVP.java @@ -27,11 +27,14 @@ 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 @@ * @author Francis Pouatcha */ public class SdJwtVP { - private String sdJwtVpString; + private final String sdJwtVpString; private final IssuerSignedJWT issuerSignedJWT; private final Map claims; @@ -52,6 +55,10 @@ public class SdJwtVP { private final Optional keyBindingJWT; + public Map getClaims() { + return claims; + } + public IssuerSignedJWT getIssuerSignedJWT() { return issuerSignedJWT; } @@ -171,7 +178,7 @@ private static JsonNode processDisclosureDigest(JsonNode node, String disclosure } public JsonNode getCnfClaim() { - return issuerSignedJWT.getPayload().get("cnf"); + return issuerSignedJWT.getCnfClaim().orElse(null); } public String present(List disclosureDigests, JsonNode keyBindingClaims, @@ -195,11 +202,31 @@ public String present(List disclosureDigests, JsonNode keyBindingClaims, 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) { @@ -262,4 +289,4 @@ public String verbose() { } return sb.toString(); } -} \ No newline at end of file +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java new file mode 100644 index 000000000000..d301a6e25270 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java @@ -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 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 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) { + }; + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java new file mode 100644 index 000000000000..9a68254651d1 --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java @@ -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 Ingrid Kamga + */ +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); + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/TestSettings.java b/core/src/test/java/org/keycloak/sdjwt/TestSettings.java index f9bdc8c5b636..8ebcc81da11f 100644 --- a/core/src/test/java/org/keycloak/sdjwt/TestSettings.java +++ b/core/src/test/java/org/keycloak/sdjwt/TestSettings.java @@ -30,7 +30,6 @@ 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,12 +40,12 @@ 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 - * + * + * open wallet foundation labs + * * @author Francis Pouatcha */ public class TestSettings { diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/KeyBindingJwtVerificationOptsTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/KeyBindingJwtVerificationOptsTest.java new file mode 100644 index 000000000000..6cb7de33208d --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/KeyBindingJwtVerificationOptsTest.java @@ -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(); + } + +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java new file mode 100644 index 000000000000..3afe4bbf61ad --- /dev/null +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java @@ -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 Ingrid Kamga + */ +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; + } +} diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java index 755c0f754ee8..9b62aaee8068 100644 --- a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/TestCompareSdJwt.java @@ -36,18 +36,15 @@ /** * 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 @@ private static void compareIssuerSignedJWT(IssuerSignedJWT e, IssuerSignedJWT a) assertEquals(e.getPayload(), a.getPayload()); - List expectedJwsStrings = Arrays.asList(e.getJwsString().split("\\.")); - List actualJwsStrings = Arrays.asList(a.getJwsString().split("\\.")); + List expectedJwsStrings = Arrays.asList(e.toJws().split("\\.")); + List actualJwsStrings = Arrays.asList(a.toJws().split("\\.")); // compare json content of header assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0))); @@ -87,7 +84,7 @@ private static void compareDisclosures(SdJwtVP expectedSdJwt, SdJwtVP actualSdJw Set expectedDisclosures = expectedSdJwt.getDisclosuresString().stream() .map(TestCompareSdJwt::toJsonNode) .collect(Collectors.toSet()); - Set actualDisclosures = expectedSdJwt.getDisclosuresString().stream() + Set actualDisclosures = actualSdJwt.getDisclosuresString().stream() .map(TestCompareSdJwt::toJsonNode) .collect(Collectors.toSet()); diff --git a/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt new file mode 100644 index 000000000000..26b6d3b0c183 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb--wrong-kb-signature.txt @@ -0,0 +1,2 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslB00 diff --git a/core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt new file mode 100644 index 000000000000..b273353d0556 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.1-sdjwt+kb.txt @@ -0,0 +1,2 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt b/core/src/test/resources/sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt new file mode 100644 index 000000000000..7f3582df43dc --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.2-sdjwt+kb--no-cnf-claim.txt @@ -0,0 +1,2 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiYWxnIjoiSFMyNTYifQ.qFD5kLKnWxuEwldxGxXRKfi3uuEokEBCglYKidyYHDM6mYrNIyYdjcCQaQ4Ll_KVpo7aLbzkAExxIZRtN3FwVQ +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt b/core/src/test/resources/sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt new file mode 100644 index 000000000000..82ec49adc7a8 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.3-sdjwt+kb--wrong-kb-typ.txt @@ -0,0 +1,2 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJ1bmV4cGVjdGVkIiwiYWxnIjoiRVMyNTYifQ.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.8LSkT5EJ4UTukkMeNDyo01yQn2hr2ipdCjXII4B8Jb56y1ZvqiE_r6fEUY1DoUa3tvKY21XzF0SCsUgCuY5PVg diff --git a/core/src/test/resources/sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt b/core/src/test/resources/sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt new file mode 100644 index 000000000000..4fe3076eb2b8 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.6-sdjwt+kb--disclosure-with-no-digest.txt @@ -0,0 +1,5 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BKwIseZm6maVGhglKOs6nwN5ooUF3uiQ +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImhlbGxvIiwgIndvcmxkIl0 +~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt new file mode 100644 index 000000000000..3e9176a81e78 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-array-elt-disclosure.txt @@ -0,0 +1,7 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifSx7Ii4uLiI6Im5vZkFmeDhTcWV2d3EwYWJWalJrV3BOai01NjBkU3dUUzdMbUJLR3FrZ2MifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCJ5IjoiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.sMAP6gUt1TLwdNtT-U06qbC4qZWB8i0gadzAHA5fvB-LpXTccHPZTsG9TIlgh8-vgYOnqr6t36XaHnU4217LpQ +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiQ00iXQ +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd +~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0 +~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt new file mode 100644 index 000000000000..b210c360fc5f --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.7-sdjwt+kb--invalid-field-disclosure.txt @@ -0,0 +1,5 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiLCJxSjROWDR3RWk1SGl6VUg4QjZ4cGZtMmxqZkVtTzlGRF9YRmtvWFd1WFdRIl0sImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDAsInN1YiI6InVzZXJfNDIiLCJuYXRpb25hbGl0aWVzIjpbeyIuLi4iOiJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0seyIuLi4iOiI3Q2Y2SmtQdWRyeTNsY2J3SGdlWjhraEF2MVUxT1NsZXJQMFZrQkpyV1owIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImNuZiI6eyJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.-lSmU8_PXTnSr1wbAkoW3Xwa_VOX-dL4MlREkWjXtOHzSJ7DnDUpv_cJSh5eub3VGqxjbHnzqz0VOoLhRx47pw +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZpZWxkIl0 +~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt new file mode 100644 index 000000000000..07e9d34753e1 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt @@ -0,0 +1,5 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJvY3QiLCJrIjoiR0F3ZXkwSlh6U0kzTUlPLS03eUt2b2R5ZC1Yam5XR2M2OWtNT1NXZ2RoNUtQeVlPdmNCQzJNa18zZndjcEFmWGVqZEQ4TVpNUE0yY2JVdWc0RERZZGQzb1ZnVjNmYlRnRnlEdDZpYTQ3SExoeUkybFNDOXJIQ1Foa0NrczRDejNyTFBtbjhGcU1BenFFQmRxQmpmTTdxOVBvTVBvRHl3cS1iU3FpcTBnQVhrbG9nMlA2OXVpa2MxX0F3dDJRdk14ZC12SGVxVGVOb2RKVGlKUllDOTQwcW5HTXNzdlhodTVsU0tKQVNuLWRzamhaX25FQlhhbmUxZGlSZFlFY2daWDJJa196amhIa044dTBJMTNDd2Y2MS1fdHJjVFRkZG9Oal9KZkVMNGpuRHJTdVBNWFFXYzNYUFBXN193U1pGMGFEdndpWnV4YnpXVjRiVVdjS1Q1Nlh3IiwiYWxnIjoiSFMyNTYifX19.hIazN1P8S71Q0mnPaOjlN6buVyFpFlwW2B1W0RDebJdpcnb-ms8sCOx5NNi8aK_5KfCkvCECfVhNVAcQpOIyFw + + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt new file mode 100644 index 000000000000..269d56388821 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt @@ -0,0 +1,3 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7ImtpZCI6ImRmZDFhYTk3LTZkOGQtNDU3NS1hMGZlLTM0Yjk2ZGUyYmZhZCJ9fQ.BLt9LcdgL-0HM1TV2OLLuJq9U1f8vlqha8I-WlcA-Je6e5U84HmWhYEgaBHOtt4NNrzAC-dk2xSxXjjr8aemTw +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt new file mode 100644 index 000000000000..cf65a6314de7 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt @@ -0,0 +1,6 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQ1oiLCJjcnYiOiJQLTI1NiIsIngiOiJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwieSI6Ilp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.MfjyETGLaL8zJ7xYiWsfhFhvEFCA2Epj7BMsZKboOtBdHw-_ap1bjUnVY_3IDvoRLmyDzb6_AUj-OJ1IQS9_Lw + + + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWDlScnJmV3RfNzBnSHpPY292R1NJdDRGbXM5VGYyZzJoamxXVklfY3haZyJ9.PWBfgGGWj5uAejIoPs4qRxCKeDyGV_Jv7jnqIylUHXiCx0qptjyxp8hHjtATQj-F9RPLuz63YEchq_yueslBeA diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt new file mode 100644 index 000000000000..13bc13735595 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps256.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJSU0EiLCJlIjoiQVFBQiIsImFsZyI6IlBTMjU2IiwibiI6ImdnUE91THM4R3U5WWVObmpfazZMQmFnOV9zNFNjdk1QRjZ4Z2t2M2ZnQnhQNVo5VWNBWUJ5Z09YczQ4UnRabzlCczhvS1NlQndXMWdHUDN2Szl1Q09iZ1cyd0JfSDQ0WG1qVk1MZnRsYnlTVzA0aHpmU3lzQWlBQ0tkdzJLZHFncEJEVDQyWHd1bEVBMV9KM3NGcWZRNkdacUwzUWRUaUpDNFZuSllSTERES0UwY2otWXlTVTZCcktfUG9WYWxqLWdOR2ZvYkRRaVJzeU0wSTVlVWpvTlM2SmxPYjlSTDlkdUh5SUdER3FrVE5kblFiaVI4Wm1NQVpyOHBPaS03WUU1dGVMVmpJUXRlOHdVcERWdk9MVXA5eVdZOE1RbjNLMUk5UTdBMGZ2bEVveTFnd1FMMkV3U29Oc1NEUDkzS1ZJdnVyYlQ1ZzVuakttZHRnZTBXSll6USJ9fX0.4LrL9rQm5GgwBT_IePfjcvwJpYgkE-s5mTUyr81kX5NblcOPdAexvojfPpnfZ2qOsv6axYkQwD3aRS5gG3oYqA + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJQUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoia2ItODdTMFlERGZUX2V5Z0pIenNyZERkZzE0dUJqXzh2MnhHa2E4WGlIMCJ9.bQXPEjBuN8I803XvX1ZK4F6FakaAb6tCo4Km5xfLXIV9ukCHySwUMRrLoP5XPVcVxBytJEJpkQ997ahs2ux3b-UN-yoBOOR6Kwc31hV7BdWU8GnSbH-6gxmB0WJPvh3fBfNfQzfTfIsTLjS9becnPoIt-1PIBQzJXGG0SHut4hjdnHEOtvnbaVwhN6Facil7A5xXoLhNsk-WBKmdBL89aFlLfpO7i1I_87uCnZXspcQ6c7kETaQReZQtJNitQrYLiFwgIv8cyiFbwPQVKwJ4XAQpt9N2I50XwTE6dhUbdAxdjRzqgoxZ-gWXMWksouyH3wrN2nKAJEs3Ya-uz1JYBQ diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt new file mode 100644 index 000000000000..69847b709de7 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt @@ -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 diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt new file mode 100644 index 000000000000..779880217bf0 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt @@ -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 diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt new file mode 100644 index 000000000000..7613f3b63c5a --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt @@ -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 diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt new file mode 100644 index 000000000000..1de3b9555cc3 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es384.txt @@ -0,0 +1,5 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJDNnJDUnJSUmM3Z3ZtUFU4S3pOVTEweXRIRW1oRVFhaTY5SkxBeDU3U0FrcHlsNlpxazVXV2FlcUFUNVdDajV0IiwiY3J2IjoiUC0zODQiLCJ4IjoiS2EyazVKRjBSZkVQMFlVU2lFODNmZ1VVS3VIRC16bWQtdXlkYXJMN1JKVjFtdGd3MkhjNU80d0ZJQm85Zk9KOSIsInkiOiJFdEIwSGV1dTlubmZjcDlCLXdGaUdWN3dCT1plTUpMTGVPaHpfUFRiUUxhdUgyTEcwQ25fRlFYajJRZURGOGwxIn19fQ.udOcVKk1WTxg5XldomVczJY2Dptiz4sFf8OQADUC0PaYzOwIl5CjMuTHhs1K-tORGfIO7nPAe_VCLC0jXaSzgQ + + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzM4NCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiaFN3cXVsbDJ4VWMtNDQwdkhLX0RUdnVXZWxDUDBlSVo0d0JOcXNOeVAySSJ9.TA93w_A3IBornn6Gu81oNjT2M-evVz6_TyCWTX-ZfL9uXkeiP44hRn0irCwCy0krtHrq49EyZxXLM2o9qRGYw1cDi68u2gYMEHLiXZzXu51q0ckQ2pjsTYDE2pqrSOZT diff --git a/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt new file mode 100644 index 000000000000..c6804e1e1079 --- /dev/null +++ b/core/src/test/resources/sdjwt/s20.8-sdjwt+kb--es512.txt @@ -0,0 +1,4 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImQiOiJBQm0xYmVXQ2xTS2ZwRTBwYk94aGVqd21XekhjdmVrMGo3N3RrWWkxYUZtclktLUtxcWxDV3FlQnZpcUwzaU5ZcnFLTEx4ZGxOOS1nNXlHZjZjUHl1MUVIIiwiY3J2IjoiUC01MjEiLCJ4IjoiQU1uY3B1bTBmZ240V2hfZUswbTFhNWdzX2MyelpVb2hGLUlvQTZ1OUhGejlyZVlxX3c4d1VMZFZaNXdySHp2MGFyOG94MmRXZWp1WDIza0FLVm8wdUZJRSIsInkiOiJBYldlOVBOd0VFUlNrU0pZRXJBektNeWNVczAwLXBlZVl2MlVFd1FYZlM5ZFZ5ZVJGMGxiU2E5WlYtZzVlWGJuRXpuUk5sa2xLcHVaeTdncVppUENPRGkyIn19fQ.QoxsCI_hP2bazbr9sS2uE93vQ1DhD8Qdrjg0csou00I8XVKbmccLlHuKHALGYEqhFWVIQ5pCSL2XCkxnz-t5uQ + +~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0 +~eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzUxMiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiWmM4aGNvVlBvN0VQTktQZzRjd05VdTFXMGtieWFwOC1zck5xWEpyUkhzOCJ9.APIud1JXrH0BLSD3TLoLQvkGS-48zqYcaB6sANnxXRDMlzHiqdqr_FnGD0QcY_VJcD_8EMhUvlrGty0qfSWMPDkHADyZIQIPTsz-5lCbPV6WU5IILprmov_PloxC-JNz58lo7Ak5hbnqJ2wZ6UAqN98XV2DMgIv84UcyezXLy23uszWm diff --git a/core/src/test/resources/sdjwt/test-settings.json b/core/src/test/resources/sdjwt/test-settings.json index 1db5b9d7d7eb..5f5121a9adc8 100644 --- a/core/src/test/resources/sdjwt/test-settings.json +++ b/core/src/test/resources/sdjwt/test-settings.json @@ -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", diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwsTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwsTest.java new file mode 100644 index 000000000000..e99837421a08 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwsTest.java @@ -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 Ingrid Kamga + */ +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()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultCryptoSdJwtVPTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPTest.java similarity index 96% rename from crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultCryptoSdJwtVPTest.java rename to crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPTest.java index 1b7e1b1e2906..9c0fb4119ba9 100644 --- a/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultCryptoSdJwtVPTest.java +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPTest.java @@ -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; diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPVerificationTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPVerificationTest.java new file mode 100644 index 000000000000..24c976c0dcd6 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVPVerificationTest.java @@ -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 Ingrid Kamga + */ +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()); + } +} diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVerificationTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVerificationTest.java new file mode 100644 index 000000000000..9ea805af24e9 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/sdjwt/DefaultCryptoSdJwtVerificationTest.java @@ -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 Ingrid Kamga + */ +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()); + } +} diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java index 48c01ec28ca7..9a61e553a44d 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/WildFlyElytronProvider.java @@ -30,8 +30,11 @@ 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; @@ -140,7 +143,7 @@ public Cipher getAesGcmCipher() throws NoSuchAlgorithmException, NoSuchPaddingEx public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException { return SecretKeyFactory.getInstance(keyAlgorithm); } - + @Override public KeyStore getKeyStore(KeystoreFormat format) throws KeyStoreException { return KeyStore.getInstance(format.toString()); @@ -165,8 +168,28 @@ public CertPathBuilder getCertPathBuilder() throws NoSuchAlgorithmException { @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 diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwsTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwsTest.java new file mode 100644 index 000000000000..5d7f820a129f --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwsTest.java @@ -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 Ingrid Kamga + */ +public class ElytronCryptoSdJwsTest extends SdJwsTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronCryptoSdJwtVPTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPTest.java similarity index 94% rename from crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronCryptoSdJwtVPTest.java rename to crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPTest.java index c81c686be2d4..75aa0633fa83 100644 --- a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/ElytronCryptoSdJwtVPTest.java +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPTest.java @@ -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; diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPVerificationTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPVerificationTest.java new file mode 100644 index 000000000000..8db7f26072e2 --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVPVerificationTest.java @@ -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 Ingrid Kamga + */ +public class ElytronCryptoSdJwtVPVerificationTest extends SdJwtVPVerificationTest { +} diff --git a/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVerificationTest.java b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVerificationTest.java new file mode 100644 index 000000000000..8910c557a3ef --- /dev/null +++ b/crypto/elytron/src/test/java/org/keycloak/crypto/elytron/test/sdjwt/ElytronCryptoSdJwtVerificationTest.java @@ -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 Ingrid Kamga + */ +public class ElytronCryptoSdJwtVerificationTest extends SdJwtVerificationTest { +}