Closes #29859 Signed-off-by: Ingrid Kamga <Ingrid.Kamga@adorsys.com>
This commit is contained in:
parent
f994cc54d5
commit
c4d6979907
27 changed files with 1788 additions and 145 deletions
43
core/src/main/java/org/keycloak/crypto/ECCurve.java
Normal file
43
core/src/main/java/org/keycloak/crypto/ECCurve.java
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.crypto;
|
||||||
|
|
||||||
|
public enum ECCurve {
|
||||||
|
P256,
|
||||||
|
P384,
|
||||||
|
P521;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert standard EC curve names (and aliases) into this enum.
|
||||||
|
*/
|
||||||
|
public static ECCurve fromStdCrv(String crv) {
|
||||||
|
switch (crv) {
|
||||||
|
case "P-256":
|
||||||
|
case "secp256r1":
|
||||||
|
return P256;
|
||||||
|
case "P-384":
|
||||||
|
case "secp384r1":
|
||||||
|
return P384;
|
||||||
|
case "P-521":
|
||||||
|
case "secp521r1":
|
||||||
|
return P521;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unexpected EC curve: " + crv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,34 +17,25 @@
|
||||||
|
|
||||||
package org.keycloak.sdjwt;
|
package org.keycloak.sdjwt;
|
||||||
|
|
||||||
import org.keycloak.crypto.SignatureVerifierContext;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for Issuer-signed JWT verification.
|
* Options for Issuer-signed JWT verification.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
*/
|
*/
|
||||||
public class IssuerSignedJwtVerificationOpts {
|
public class IssuerSignedJwtVerificationOpts {
|
||||||
private final SignatureVerifierContext verifier;
|
|
||||||
private final boolean validateIssuedAtClaim;
|
private final boolean validateIssuedAtClaim;
|
||||||
private final boolean validateExpirationClaim;
|
private final boolean validateExpirationClaim;
|
||||||
private final boolean validateNotBeforeClaim;
|
private final boolean validateNotBeforeClaim;
|
||||||
|
|
||||||
public IssuerSignedJwtVerificationOpts(
|
public IssuerSignedJwtVerificationOpts(
|
||||||
SignatureVerifierContext verifier,
|
|
||||||
boolean validateIssuedAtClaim,
|
boolean validateIssuedAtClaim,
|
||||||
boolean validateExpirationClaim,
|
boolean validateExpirationClaim,
|
||||||
boolean validateNotBeforeClaim) {
|
boolean validateNotBeforeClaim) {
|
||||||
this.verifier = verifier;
|
|
||||||
this.validateIssuedAtClaim = validateIssuedAtClaim;
|
this.validateIssuedAtClaim = validateIssuedAtClaim;
|
||||||
this.validateExpirationClaim = validateExpirationClaim;
|
this.validateExpirationClaim = validateExpirationClaim;
|
||||||
this.validateNotBeforeClaim = validateNotBeforeClaim;
|
this.validateNotBeforeClaim = validateNotBeforeClaim;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignatureVerifierContext getVerifier() {
|
|
||||||
return verifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean mustValidateIssuedAtClaim() {
|
public boolean mustValidateIssuedAtClaim() {
|
||||||
return validateIssuedAtClaim;
|
return validateIssuedAtClaim;
|
||||||
}
|
}
|
||||||
|
@ -62,16 +53,10 @@ public class IssuerSignedJwtVerificationOpts {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private SignatureVerifierContext verifier;
|
|
||||||
private boolean validateIssuedAtClaim;
|
private boolean validateIssuedAtClaim;
|
||||||
private boolean validateExpirationClaim = true;
|
private boolean validateExpirationClaim = true;
|
||||||
private boolean validateNotBeforeClaim = true;
|
private boolean validateNotBeforeClaim = true;
|
||||||
|
|
||||||
public Builder withVerifier(SignatureVerifierContext verifier) {
|
|
||||||
this.verifier = verifier;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder withValidateIssuedAtClaim(boolean validateIssuedAtClaim) {
|
public Builder withValidateIssuedAtClaim(boolean validateIssuedAtClaim) {
|
||||||
this.validateIssuedAtClaim = validateIssuedAtClaim;
|
this.validateIssuedAtClaim = validateIssuedAtClaim;
|
||||||
return this;
|
return this;
|
||||||
|
@ -89,7 +74,6 @@ public class IssuerSignedJwtVerificationOpts {
|
||||||
|
|
||||||
public IssuerSignedJwtVerificationOpts build() {
|
public IssuerSignedJwtVerificationOpts build() {
|
||||||
return new IssuerSignedJwtVerificationOpts(
|
return new IssuerSignedJwtVerificationOpts(
|
||||||
verifier,
|
|
||||||
validateIssuedAtClaim,
|
validateIssuedAtClaim,
|
||||||
validateExpirationClaim,
|
validateExpirationClaim,
|
||||||
validateNotBeforeClaim
|
validateNotBeforeClaim
|
||||||
|
|
98
core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java
Normal file
98
core/src/main/java/org/keycloak/sdjwt/JwkParsingUtils.java
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sdjwt;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
|
||||||
|
import org.keycloak.crypto.ECCurve;
|
||||||
|
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.util.JWKSUtils;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class JwkParsingUtils {
|
||||||
|
|
||||||
|
public static SignatureVerifierContext convertJwkNodeToVerifierContext(JsonNode jwkNode) {
|
||||||
|
JWK jwk;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jwk = SdJwtUtils.mapper.convertValue(jwkNode, JWK.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("Malformed JWK");
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertJwkToVerifierContext(jwk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignatureVerifierContext convertJwkToVerifierContext(JWK jwk) {
|
||||||
|
// Wrap JWK
|
||||||
|
|
||||||
|
KeyWrapper keyWrapper;
|
||||||
|
|
||||||
|
try {
|
||||||
|
keyWrapper = JWKSUtils.getKeyWrapper(jwk);
|
||||||
|
Objects.requireNonNull(keyWrapper);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("Unsupported or invalid JWK");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build verifier
|
||||||
|
|
||||||
|
// KeyType.EC
|
||||||
|
if (keyWrapper.getType().equals(KeyType.EC)) {
|
||||||
|
if (keyWrapper.getAlgorithm() == null) {
|
||||||
|
Objects.requireNonNull(keyWrapper.getCurve());
|
||||||
|
|
||||||
|
String alg = null;
|
||||||
|
switch (ECCurve.fromStdCrv(keyWrapper.getCurve())) {
|
||||||
|
case P256:
|
||||||
|
alg = Algorithm.ES256;
|
||||||
|
break;
|
||||||
|
case P384:
|
||||||
|
alg = Algorithm.ES384;
|
||||||
|
break;
|
||||||
|
case P521:
|
||||||
|
alg = Algorithm.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 IllegalArgumentException("Unexpected key type: " + keyWrapper.getType());
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,9 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public abstract class SdJws {
|
public abstract class SdJws {
|
||||||
|
|
||||||
|
public static final String CLAIM_NAME_ISSUER = "iss";
|
||||||
|
|
||||||
private final JWSInput jwsInput;
|
private final JWSInput jwsInput;
|
||||||
private final JsonNode payload;
|
private final JsonNode payload;
|
||||||
|
|
||||||
|
@ -158,7 +161,7 @@ public abstract class SdJws {
|
||||||
* @param issuers List of trusted issuers
|
* @param issuers List of trusted issuers
|
||||||
*/
|
*/
|
||||||
public void verifyIssClaim(List<String> issuers) throws VerificationException {
|
public void verifyIssClaim(List<String> issuers) throws VerificationException {
|
||||||
verifyClaimAgainstTrustedValues(issuers, "iss");
|
verifyClaimAgainstTrustedValues(issuers, CLAIM_NAME_ISSUER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,6 +26,7 @@ import java.util.stream.IntStream;
|
||||||
|
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
@ -44,6 +45,7 @@ public class SdJwt {
|
||||||
private final IssuerSignedJWT issuerSignedJWT;
|
private final IssuerSignedJWT issuerSignedJWT;
|
||||||
private final List<SdJwtClaim> claims;
|
private final List<SdJwtClaim> claims;
|
||||||
private final List<String> disclosures = new ArrayList<>();
|
private final List<String> disclosures = new ArrayList<>();
|
||||||
|
private final SdJwtVerificationContext sdJwtVerificationContext;
|
||||||
|
|
||||||
private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List<SdJwt> nesteSdJwts,
|
private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List<SdJwt> nesteSdJwts,
|
||||||
Optional<KeyBindingJWT> keyBindingJWT,
|
Optional<KeyBindingJWT> keyBindingJWT,
|
||||||
|
@ -65,6 +67,12 @@ public class SdJwt {
|
||||||
|
|
||||||
nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures()));
|
nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures()));
|
||||||
this.disclosures.addAll(getDisclosureStrings(claims));
|
this.disclosures.addAll(getDisclosureStrings(claims));
|
||||||
|
|
||||||
|
// Instantiate context for verification
|
||||||
|
this.sdJwtVerificationContext = new SdJwtVerificationContext(
|
||||||
|
this.issuerSignedJWT,
|
||||||
|
this.disclosures
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<String> sdJwtString = Optional.empty();
|
private Optional<String> sdJwtString = Optional.empty();
|
||||||
|
@ -198,14 +206,21 @@ public class SdJwt {
|
||||||
/**
|
/**
|
||||||
* Verifies SD-JWT as to whether the Issuer-signed JWT's signature and disclosures are valid.
|
* 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
|
* @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller
|
||||||
* must be specified for validating the Issuer-signed JWT. The caller
|
* is responsible for establishing trust in that the keys belong
|
||||||
* is responsible for establishing trust in that associated public keys
|
* to the intended issuer.
|
||||||
* belong to the intended issuer.
|
* @param verificationOpts Options to parameterize the Issuer-Signed JWT verification.
|
||||||
* @throws VerificationException if verification failed
|
* @throws VerificationException if verification failed
|
||||||
*/
|
*/
|
||||||
public void verify(IssuerSignedJwtVerificationOpts verificationOpts) throws VerificationException {
|
public void verify(
|
||||||
new SdJwtVerificationContext(issuerSignedJWT, disclosures).verifyIssuance(verificationOpts);
|
List<SignatureVerifierContext> issuerVerifyingKeys,
|
||||||
|
IssuerSignedJwtVerificationOpts verificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
sdJwtVerificationContext.verifyIssuance(
|
||||||
|
issuerVerifyingKeys,
|
||||||
|
verificationOpts,
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// builder for SdJwt
|
// builder for SdJwt
|
||||||
|
|
|
@ -20,22 +20,19 @@ package org.keycloak.sdjwt;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.VerificationException;
|
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.crypto.SignatureVerifierContext;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.sdjwt.consumer.PresentationRequirements;
|
||||||
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
import org.keycloak.sdjwt.vp.KeyBindingJWT;
|
||||||
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
import org.keycloak.util.JWKSUtils;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -48,6 +45,9 @@ import java.util.stream.Collectors;
|
||||||
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
*/
|
*/
|
||||||
public class SdJwtVerificationContext {
|
public class SdJwtVerificationContext {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(SdJwtVerificationContext.class.getName());
|
||||||
|
|
||||||
private String sdJwtVpString;
|
private String sdJwtVpString;
|
||||||
|
|
||||||
private final IssuerSignedJWT issuerSignedJwt;
|
private final IssuerSignedJWT issuerSignedJwt;
|
||||||
|
@ -79,7 +79,7 @@ public class SdJwtVerificationContext {
|
||||||
.map(disclosureString -> {
|
.map(disclosureString -> {
|
||||||
String digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
String digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
||||||
disclosureString.getBytes(), issuerSignedJwt.getSdHashAlg());
|
disclosureString.getBytes(), issuerSignedJwt.getSdHashAlg());
|
||||||
return new AbstractMap.SimpleEntry<String,String>(digest, disclosureString);
|
return new AbstractMap.SimpleEntry<>(digest, disclosureString);
|
||||||
})
|
})
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
}
|
}
|
||||||
|
@ -92,17 +92,21 @@ public class SdJwtVerificationContext {
|
||||||
* - all Disclosures are valid and correspond to a respective digest value in the Issuer-signed JWT
|
* - 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).
|
* (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
|
* @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller
|
||||||
* must be specified for validating the Issuer-signed JWT. The caller
|
* is responsible for establishing trust in that the keys belong
|
||||||
* is responsible for establishing trust in that associated public keys
|
* to the intended issuer.
|
||||||
* belong to the intended issuer.
|
* @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification.
|
||||||
|
* @param presentationRequirements If set, the presentation requirements will be enforced upon fully
|
||||||
|
* disclosing the Issuer-signed JWT during the verification.
|
||||||
* @throws VerificationException if verification failed
|
* @throws VerificationException if verification failed
|
||||||
*/
|
*/
|
||||||
public void verifyIssuance(
|
public void verifyIssuance(
|
||||||
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts
|
List<SignatureVerifierContext> issuerVerifyingKeys,
|
||||||
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
||||||
|
PresentationRequirements presentationRequirements
|
||||||
) throws VerificationException {
|
) throws VerificationException {
|
||||||
// Validate the Issuer-signed JWT.
|
// Validate the Issuer-signed JWT.
|
||||||
validateIssuerSignedJwt(issuerSignedJwtVerificationOpts.getVerifier());
|
validateIssuerSignedJwt(issuerVerifyingKeys);
|
||||||
|
|
||||||
// Validate disclosures.
|
// Validate disclosures.
|
||||||
JsonNode disclosedPayload = validateDisclosuresDigests();
|
JsonNode disclosedPayload = validateDisclosuresDigests();
|
||||||
|
@ -112,6 +116,11 @@ public class SdJwtVerificationContext {
|
||||||
// SD-JWT payload, but there is no guarantee they would do so. Therefore, Verifiers cannot reliably
|
// 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.
|
// depend on that and need to operate as though security-critical claims might be selectively disclosable.
|
||||||
validateIssuerSignedJwtTimeClaims(disclosedPayload, issuerSignedJwtVerificationOpts);
|
validateIssuerSignedJwtTimeClaims(disclosedPayload, issuerSignedJwtVerificationOpts);
|
||||||
|
|
||||||
|
// Enforce presentation requirements.
|
||||||
|
if (presentationRequirements != null) {
|
||||||
|
presentationRequirements.checkIfSatisfiedBy(disclosedPayload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,18 +131,22 @@ public class SdJwtVerificationContext {
|
||||||
* to ensure that if Key Binding is required, the Key Binding JWT is signed by the Holder and valid.
|
* to ensure that if Key Binding is required, the Key Binding JWT is signed by the Holder and valid.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification. A verifier
|
* @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller
|
||||||
* must be specified for validating the Issuer-signed JWT. The caller
|
* is responsible for establishing trust in that the keys belong
|
||||||
* is responsible for establishing trust in that associated public keys
|
* to the intended issuer.
|
||||||
* belong to the intended issuer.
|
* @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification.
|
||||||
* @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification.
|
* @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification.
|
||||||
* Must, among others, specify the Verifier's policy whether
|
* Must, among others, specify the Verifier's policy whether
|
||||||
* to check Key Binding.
|
* to check Key Binding.
|
||||||
|
* @param presentationRequirements If set, the presentation requirements will be enforced upon fully
|
||||||
|
* disclosing the Issuer-signed JWT during the verification.
|
||||||
* @throws VerificationException if verification failed
|
* @throws VerificationException if verification failed
|
||||||
*/
|
*/
|
||||||
public void verifyPresentation(
|
public void verifyPresentation(
|
||||||
|
List<SignatureVerifierContext> issuerVerifyingKeys,
|
||||||
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
||||||
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts,
|
||||||
|
PresentationRequirements presentationRequirements
|
||||||
) throws VerificationException {
|
) throws VerificationException {
|
||||||
// If Key Binding is required and a Key Binding JWT is not provided,
|
// If Key Binding is required and a Key Binding JWT is not provided,
|
||||||
// the Verifier MUST reject the Presentation.
|
// the Verifier MUST reject the Presentation.
|
||||||
|
@ -142,7 +155,7 @@ public class SdJwtVerificationContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upon receiving a Presentation, in addition to the checks in {@link #verifyIssuance}...
|
// Upon receiving a Presentation, in addition to the checks in {@link #verifyIssuance}...
|
||||||
verifyIssuance(issuerSignedJwtVerificationOpts);
|
verifyIssuance(issuerVerifyingKeys, issuerSignedJwtVerificationOpts, presentationRequirements);
|
||||||
|
|
||||||
// Validate Key Binding JWT if required
|
// Validate Key Binding JWT if required
|
||||||
if (keyBindingJwtVerificationOpts.isKeyBindingRequired()) {
|
if (keyBindingJwtVerificationOpts.isKeyBindingRequired()) {
|
||||||
|
@ -158,19 +171,33 @@ public class SdJwtVerificationContext {
|
||||||
* - the Issuer-signed JWT is valid, i.e., it is signed by the Issuer and the signature is valid
|
* - the Issuer-signed JWT is valid, i.e., it is signed by the Issuer and the signature is valid
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
|
* @param verifiers Verifying keys for validating the Issuer-signed JWT.
|
||||||
* @throws VerificationException if verification failed
|
* @throws VerificationException if verification failed
|
||||||
*/
|
*/
|
||||||
private void validateIssuerSignedJwt(SignatureVerifierContext verifier) throws VerificationException {
|
private void validateIssuerSignedJwt(
|
||||||
|
List<SignatureVerifierContext> verifiers
|
||||||
|
) throws VerificationException {
|
||||||
// Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure
|
// Check that the _sd_alg claim value is understood and the hash algorithm is deemed secure
|
||||||
issuerSignedJwt.verifySdHashAlgorithm();
|
issuerSignedJwt.verifySdHashAlgorithm();
|
||||||
|
|
||||||
// Validate the signature over the Issuer-signed JWT
|
// Validate the signature over the Issuer-signed JWT
|
||||||
|
Iterator<SignatureVerifierContext> iterator = verifiers.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
try {
|
try {
|
||||||
|
SignatureVerifierContext verifier = iterator.next();
|
||||||
issuerSignedJwt.verifySignature(verifier);
|
issuerSignedJwt.verifySignature(verifier);
|
||||||
|
return;
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
throw new VerificationException("Invalid Issuer-Signed JWT", e);
|
logger.debugf(e, "Issuer-signed JWT's signature verification failed against one potential verifying key");
|
||||||
|
if (iterator.hasNext()) {
|
||||||
|
logger.debugf("Retrying Issuer-signed JWT's signature verification with next potential verifying key");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No potential verifier could verify the JWT's signature
|
||||||
|
throw new VerificationException("Invalid Issuer-Signed JWT: Signature could not be verified");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate Key Binding JWT
|
* Validate Key Binding JWT
|
||||||
|
@ -241,51 +268,12 @@ public class SdJwtVerificationContext {
|
||||||
throw new UnsupportedOperationException("Only cnf/jwk claim supported");
|
throw new UnsupportedOperationException("Only cnf/jwk claim supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JWK
|
// Convert JWK
|
||||||
KeyWrapper keyWrapper;
|
|
||||||
try {
|
try {
|
||||||
JWK jwk = SdJwtUtils.mapper.convertValue(cnfJwk, JWK.class);
|
return JwkParsingUtils.convertJwkNodeToVerifierContext(cnfJwk);
|
||||||
keyWrapper = JWKSUtils.getKeyWrapper(jwk);
|
|
||||||
Objects.requireNonNull(keyWrapper);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new VerificationException("Malformed or unsupported cnf/jwk claim");
|
throw new VerificationException("Could not process cnf/jwk", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -586,10 +574,10 @@ public class SdJwtVerificationContext {
|
||||||
|
|
||||||
// If the claim name is _sd or ..., the SD-JWT MUST be rejected.
|
// If the claim name is _sd or ..., the SD-JWT MUST be rejected.
|
||||||
|
|
||||||
List<String> denylist = Arrays.asList(new String[]{
|
List<String> denylist = Arrays.asList(
|
||||||
IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE,
|
IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE,
|
||||||
UndisclosedArrayElement.SD_CLAIM_NAME
|
UndisclosedArrayElement.SD_CLAIM_NAME
|
||||||
});
|
);
|
||||||
|
|
||||||
String claimName = arrayNode.get(1).asText();
|
String claimName = arrayNode.get(1).asText();
|
||||||
if (denylist.contains(claimName)) {
|
if (denylist.contains(claimName)) {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sdjwt.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public interface HttpDataFetcher {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an HTTP GET at the URI and parses the response as JSON
|
||||||
|
* @throws IOException if I/O error or HTTP status not OK (200)
|
||||||
|
*/
|
||||||
|
JsonNode fetchJsonData(String uri) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sdjwt.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POJO for JWT VC Metadata
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
* @see <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-05#name-jwt-vc-issuer-metadata-resp">
|
||||||
|
* JWT VC Issuer Metadata Response
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class JwtVcMetadata {
|
||||||
|
|
||||||
|
@JsonProperty("issuer")
|
||||||
|
private String issuer;
|
||||||
|
|
||||||
|
@JsonProperty("jwks_uri")
|
||||||
|
private String jwksUri;
|
||||||
|
|
||||||
|
@JsonProperty("jwks")
|
||||||
|
private JSONWebKeySet jwks;
|
||||||
|
|
||||||
|
public String getIssuer() {
|
||||||
|
return issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIssuer(String issuer) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJwksUri() {
|
||||||
|
return jwksUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJwksUri(String jwksUri) {
|
||||||
|
this.jwksUri = jwksUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONWebKeySet getJwks() {
|
||||||
|
return jwks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJwks(JSONWebKeySet jwks) {
|
||||||
|
this.jwks = jwks;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
/*
|
||||||
|
* 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.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
|
import org.keycloak.jose.jwk.JWK;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
|
import org.keycloak.sdjwt.JwkParsingUtils;
|
||||||
|
import org.keycloak.sdjwt.SdJws;
|
||||||
|
import org.keycloak.sdjwt.SdJwtUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trusted Issuer for running SD-JWT VP verification.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation targets issuers exposing verifying keys on a normalized JWT VC Issuer metadata endpoint.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
* @see <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-05#name-issuer-signed-jwt-verificat">
|
||||||
|
* JWT VC Issuer Metadata
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class JwtVcMetadataTrustedSdJwtIssuer implements TrustedSdJwtIssuer {
|
||||||
|
|
||||||
|
private static final String JWT_VC_ISSUER_END_POINT = "/.well-known/jwt-vc-issuer";
|
||||||
|
|
||||||
|
private final Pattern issuerUriPattern;
|
||||||
|
private final HttpDataFetcher httpDataFetcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param issuerUri a trusted issuer URI
|
||||||
|
*/
|
||||||
|
public JwtVcMetadataTrustedSdJwtIssuer(String issuerUri, HttpDataFetcher httpDataFetcher) {
|
||||||
|
try {
|
||||||
|
validateHttpsIssuerUri(issuerUri);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a Regex pattern to only match the argument URI
|
||||||
|
this.issuerUriPattern = Pattern.compile(Pattern.quote(issuerUri));
|
||||||
|
|
||||||
|
// Assign HttpDataFetcher implementation
|
||||||
|
this.httpDataFetcher = httpDataFetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param issuerUriPattern a regex pattern for trusted issuer URIs
|
||||||
|
*/
|
||||||
|
public JwtVcMetadataTrustedSdJwtIssuer(Pattern issuerUriPattern, HttpDataFetcher httpDataFetcher) {
|
||||||
|
this.issuerUriPattern = issuerUriPattern;
|
||||||
|
this.httpDataFetcher = httpDataFetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SignatureVerifierContext> resolveIssuerVerifyingKeys(IssuerSignedJWT issuerSignedJWT)
|
||||||
|
throws VerificationException {
|
||||||
|
// Read iss (claim) and kid (header)
|
||||||
|
String iss = Optional.ofNullable(issuerSignedJWT.getPayload().get(SdJws.CLAIM_NAME_ISSUER))
|
||||||
|
.map(JsonNode::asText)
|
||||||
|
.orElse("");
|
||||||
|
String kid = issuerSignedJWT.getHeader().getKeyId();
|
||||||
|
|
||||||
|
// Match the read iss claim against the trusted pattern
|
||||||
|
Matcher matcher = issuerUriPattern.matcher(iss);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
throw new VerificationException(String.format(
|
||||||
|
"Unexpected Issuer URI claim. Expected=/%s/, Got=%s",
|
||||||
|
issuerUriPattern.pattern(), iss
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// As per specs, only HTTPS URIs are supported
|
||||||
|
validateHttpsIssuerUri(iss);
|
||||||
|
|
||||||
|
// Fetch exposed JWKs
|
||||||
|
List<JWK> jwks = fetchIssuerMetadataJwks(iss);
|
||||||
|
if (jwks.isEmpty()) {
|
||||||
|
throw new VerificationException(
|
||||||
|
String.format("Issuer JWKs were unexpectedly resolved to an empty list. Issuer URI: %s", iss)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If kid specified, only consider the (single) matching key
|
||||||
|
if (kid != null) {
|
||||||
|
List<JWK> matchingJwks = jwks.stream()
|
||||||
|
.filter(jwk -> {
|
||||||
|
String jwkKid = jwk.getKeyId();
|
||||||
|
return jwkKid != null && jwkKid.equals(kid);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (matchingJwks.isEmpty()) {
|
||||||
|
throw new VerificationException(
|
||||||
|
String.format("No published JWK was found to match kid: %s", kid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingJwks.size() > 1) {
|
||||||
|
throw new VerificationException(
|
||||||
|
String.format("Cannot choose between multiple exposed JWKs with same kid: %s", kid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
jwks = Collections.singletonList(matchingJwks.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build SignatureVerifierContext's
|
||||||
|
List<SignatureVerifierContext> verifiers = new ArrayList<>();
|
||||||
|
for (JWK jwk : jwks) {
|
||||||
|
try {
|
||||||
|
verifiers.add(JwkParsingUtils.convertJwkToVerifierContext(jwk));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new VerificationException("A potential JWK was retrieved but found invalid", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateHttpsIssuerUri(String issuerUri) throws VerificationException {
|
||||||
|
if (!issuerUri.startsWith("https://")) {
|
||||||
|
throw new VerificationException(
|
||||||
|
"HTTPS URI required to retrieve JWT VC Issuer Metadata"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<JWK> fetchIssuerMetadataJwks(String issuerUri) throws VerificationException {
|
||||||
|
// Build full URL to JWT VC metadata endpoint
|
||||||
|
|
||||||
|
issuerUri = normalizeUri(issuerUri);
|
||||||
|
String jwtVcIssuerUri = issuerUri
|
||||||
|
.concat(JWT_VC_ISSUER_END_POINT); // Append well-known path
|
||||||
|
|
||||||
|
// Fetch and parse metadata
|
||||||
|
|
||||||
|
JwtVcMetadata issuerMetadata;
|
||||||
|
JsonNode issuerMetadataNode = fetchData(jwtVcIssuerUri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
issuerMetadata = SdJwtUtils.mapper.treeToValue(issuerMetadataNode, JwtVcMetadata.class);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new VerificationException("Failed to parse exposed JWT VC Metadata", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate metadata
|
||||||
|
|
||||||
|
String exposedIssuerUri = normalizeUri(issuerMetadata.getIssuer());
|
||||||
|
|
||||||
|
if (!issuerUri.equals(exposedIssuerUri)) {
|
||||||
|
throw new VerificationException(String.format(
|
||||||
|
"Unexpected metadata's issuer. Expected=%s, Got=%s",
|
||||||
|
issuerUri, exposedIssuerUri
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract exposed JWKS (including dereferencing if necessary)
|
||||||
|
|
||||||
|
String jwksUri = issuerMetadata.getJwksUri();
|
||||||
|
JSONWebKeySet jwks = issuerMetadata.getJwks();
|
||||||
|
|
||||||
|
if (jwks == null && jwksUri != null) {
|
||||||
|
// Dereference JWKS URI
|
||||||
|
JsonNode jwksNode = fetchData(jwksUri);
|
||||||
|
|
||||||
|
// Parse fetched JWKS
|
||||||
|
try {
|
||||||
|
jwks = SdJwtUtils.mapper.treeToValue(jwksNode, JSONWebKeySet.class);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new VerificationException("Failed to parse exposed JWKS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwks == null || jwks.getKeys() == null) {
|
||||||
|
throw new VerificationException(
|
||||||
|
String.format("Could not resolve issuer JWKs with URI: %s", issuerUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.asList(jwks.getKeys());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode fetchData(String uri) throws VerificationException {
|
||||||
|
try {
|
||||||
|
return Objects.requireNonNull(httpDataFetcher.fetchJsonData(uri));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new VerificationException(
|
||||||
|
String.format("Could not fetch data from URI: %s", uri),
|
||||||
|
exception
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeUri(String uri) {
|
||||||
|
// Remove any trailing slash
|
||||||
|
return uri.replaceAll("/$", "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sdjwt.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presentation requirements to constrain the kind of credential expected.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This mirrors the idea of the expressive
|
||||||
|
* <a href="https://identity.foundation/presentation-exchange/#presentation-definition">DIF Presentation Definition</a>,
|
||||||
|
* while enabling simpler alternatives.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public interface PresentationRequirements {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the configured requirements are satisfied by the presentation.
|
||||||
|
*
|
||||||
|
* @param disclosedPayload The fully disclosed Issuer-signed JWT of the presented token.
|
||||||
|
* @throws VerificationException if the configured requirements are not satisfied.
|
||||||
|
*/
|
||||||
|
void checkIfSatisfiedBy(JsonNode disclosedPayload) throws VerificationException;
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sdjwt.consumer;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
|
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component for consuming (verifying) SD-JWT presentations.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The purpose is to streamline SD-JWT VP verification beyond signature
|
||||||
|
* and disclosure checks of {@link org.keycloak.sdjwt.SdJwtVerificationContext}
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class SdJwtPresentationConsumer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify SD-JWT presentation against specific requirements.
|
||||||
|
*
|
||||||
|
* @param sdJwtVP the presentation to verify
|
||||||
|
* @param presentationRequirements the requirements on presented claims
|
||||||
|
* @param trustedSdJwtIssuers trusted issuers for the verification
|
||||||
|
* @param issuerSignedJwtVerificationOpts policy for Issuer-signed JWT verification
|
||||||
|
* @param keyBindingJwtVerificationOpts policy for Key-binding JWT verification
|
||||||
|
* @throws VerificationException if the verification fails for some reason
|
||||||
|
*/
|
||||||
|
public void verifySdJwtPresentation(
|
||||||
|
SdJwtVP sdJwtVP,
|
||||||
|
PresentationRequirements presentationRequirements,
|
||||||
|
List<TrustedSdJwtIssuer> trustedSdJwtIssuers,
|
||||||
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
||||||
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
|
) throws VerificationException {
|
||||||
|
// Retrieve verifying keys for Issuer-signed JWT
|
||||||
|
IssuerSignedJWT issuerSignedJWT = sdJwtVP.getIssuerSignedJWT();
|
||||||
|
List<SignatureVerifierContext> issuerVerifyingKeys = new ArrayList<>();
|
||||||
|
for (TrustedSdJwtIssuer trustedSdJwtIssuer : trustedSdJwtIssuers) {
|
||||||
|
List<SignatureVerifierContext> keys = trustedSdJwtIssuer
|
||||||
|
.resolveIssuerVerifyingKeys(issuerSignedJWT);
|
||||||
|
issuerVerifyingKeys.addAll(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the SD-JWT token cryptographically
|
||||||
|
// Pass presentation requirements to enforce that the presented token meets them
|
||||||
|
sdJwtVP.getSdJwtVerificationContext()
|
||||||
|
.verifyPresentation(
|
||||||
|
issuerVerifyingKeys,
|
||||||
|
issuerSignedJwtVerificationOpts,
|
||||||
|
keyBindingJwtVerificationOpts,
|
||||||
|
presentationRequirements
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* 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.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple presentation definition of the kind of credential expected.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The credential's type and required claims are configured using regex patterns.
|
||||||
|
* The values of these fields are JSON-ified prior to matching the regex pattern.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class SimplePresentationDefinition implements PresentationRequirements {
|
||||||
|
|
||||||
|
private final Map<String, Pattern> requirements;
|
||||||
|
|
||||||
|
public SimplePresentationDefinition(Map<String, Pattern> requirements) {
|
||||||
|
this.requirements = requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the provided JSON payload satisfies all required field patterns.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For each required field, the corresponding JSON field value in the disclosed Issuer-signed JWT's payload
|
||||||
|
* is matched against the associated regex pattern. If any required field is missing or does not match the
|
||||||
|
* pattern, a {@link VerificationException} is thrown.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param disclosedPayload The fully disclosed Issuer-signed JWT of the presented token.
|
||||||
|
* @throws VerificationException If any required field is missing or fails the pattern check.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void checkIfSatisfiedBy(JsonNode disclosedPayload) throws VerificationException {
|
||||||
|
for (Map.Entry<String, Pattern> requirement : requirements.entrySet()) {
|
||||||
|
String field = requirement.getKey();
|
||||||
|
Pattern pattern = requirement.getValue();
|
||||||
|
|
||||||
|
// Retrieve the value of the required field from the payload
|
||||||
|
JsonNode presented = disclosedPayload.get(field);
|
||||||
|
|
||||||
|
// Check if the required field is present in the payload
|
||||||
|
if (presented == null || presented.isNull()) {
|
||||||
|
throw new VerificationException(
|
||||||
|
String.format("A required field was not presented: `%s`", field)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the JSON representation of the field's value
|
||||||
|
String json = presented.toString();
|
||||||
|
|
||||||
|
// Match the field value against the configured regex pattern
|
||||||
|
Matcher matcher = pattern.matcher(json);
|
||||||
|
if (!matcher.matches()) {
|
||||||
|
throw new VerificationException(String.format(
|
||||||
|
"Pattern matching failed for required field: `%s`. Expected pattern: /%s/, but got: %s",
|
||||||
|
field, pattern.pattern(), json
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final Map<String, Pattern> requirements = new HashMap<>();
|
||||||
|
|
||||||
|
public Builder addClaimRequirement(String field, String regexPattern) {
|
||||||
|
this.requirements.put(field, Pattern.compile(regexPattern));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimplePresentationDefinition build() {
|
||||||
|
return new SimplePresentationDefinition(requirements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.consumer;
|
||||||
|
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trusted Issuer for running SD-JWT VP verification.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class StaticTrustedSdJwtIssuer implements TrustedSdJwtIssuer {
|
||||||
|
|
||||||
|
private final List<SignatureVerifierContext> signatureVerifierContexts;
|
||||||
|
|
||||||
|
public StaticTrustedSdJwtIssuer(List<SignatureVerifierContext> signatureVerifierContexts) {
|
||||||
|
this.signatureVerifierContexts = signatureVerifierContexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SignatureVerifierContext> resolveIssuerVerifyingKeys(IssuerSignedJWT issuerSignedJWT) {
|
||||||
|
return signatureVerifierContexts;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sdjwt.consumer;
|
||||||
|
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trusted Issuer for running SD-JWT VP verification.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public interface TrustedSdJwtIssuer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves potential verifying keys to validate the Issuer-signed JWT.
|
||||||
|
* The method ensures that the resolved public keys can be trusted.
|
||||||
|
*
|
||||||
|
* @param issuerSignedJWT The Issuer-signed JWT to validate.
|
||||||
|
* @return trusted verifying keys
|
||||||
|
* @throws VerificationException if no trustworthy verifying key could be resolved
|
||||||
|
*/
|
||||||
|
List<SignatureVerifierContext> resolveIssuerVerifyingKeys(IssuerSignedJWT issuerSignedJWT)
|
||||||
|
throws VerificationException;
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import java.util.Set;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
import org.keycloak.sdjwt.IssuerSignedJWT;
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
||||||
import org.keycloak.sdjwt.SdJwt;
|
import org.keycloak.sdjwt.SdJwt;
|
||||||
|
@ -54,6 +55,7 @@ public class SdJwtVP {
|
||||||
private final String hashAlgorithm;
|
private final String hashAlgorithm;
|
||||||
|
|
||||||
private final Optional<KeyBindingJWT> keyBindingJWT;
|
private final Optional<KeyBindingJWT> keyBindingJWT;
|
||||||
|
private final SdJwtVerificationContext sdJwtVerificationContext;
|
||||||
|
|
||||||
public Map<String, ArrayNode> getClaims() {
|
public Map<String, ArrayNode> getClaims() {
|
||||||
return claims;
|
return claims;
|
||||||
|
@ -98,6 +100,14 @@ public class SdJwtVP {
|
||||||
this.recursiveDigests = Collections.unmodifiableMap(recursiveDigests);
|
this.recursiveDigests = Collections.unmodifiableMap(recursiveDigests);
|
||||||
this.ghostDigests = Collections.unmodifiableList(ghostDigests);
|
this.ghostDigests = Collections.unmodifiableList(ghostDigests);
|
||||||
this.keyBindingJWT = keyBindingJWT;
|
this.keyBindingJWT = keyBindingJWT;
|
||||||
|
|
||||||
|
// Instantiate context for verification
|
||||||
|
this.sdJwtVerificationContext = new SdJwtVerificationContext(
|
||||||
|
this.sdJwtVpString,
|
||||||
|
this.issuerSignedJWT,
|
||||||
|
this.disclosures,
|
||||||
|
this.keyBindingJWT.orElse(null)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SdJwtVP of(String sdJwtString) {
|
public static SdJwtVP of(String sdJwtString) {
|
||||||
|
@ -217,21 +227,33 @@ public class SdJwtVP {
|
||||||
/**
|
/**
|
||||||
* Verifies SD-JWT presentation.
|
* Verifies SD-JWT presentation.
|
||||||
*
|
*
|
||||||
* @param issuerSignedJwtVerificationOpts Options to parameterize the verification. A verifier must be specified
|
* @param issuerVerifyingKeys Verifying keys for validating the Issuer-signed JWT. The caller
|
||||||
* for validating the Issuer-signed JWT. The caller is responsible for
|
* is responsible for establishing trust in that the keys belong
|
||||||
* establishing trust in that associated public keys belong to the
|
* to the intended issuer.
|
||||||
* intended issuer.
|
* @param issuerSignedJwtVerificationOpts Options to parameterize the Issuer-Signed JWT verification.
|
||||||
* @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification.
|
* @param keyBindingJwtVerificationOpts Options to parameterize the Key Binding JWT verification.
|
||||||
* Must, among others, specify the Verifier's policy whether
|
* Must, among others, specify the Verifier's policy whether
|
||||||
* to check Key Binding.
|
* to check Key Binding.
|
||||||
* @throws VerificationException if verification failed
|
* @throws VerificationException if verification failed
|
||||||
*/
|
*/
|
||||||
public void verify(
|
public void verify(
|
||||||
|
List<SignatureVerifierContext> issuerVerifyingKeys,
|
||||||
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts,
|
||||||
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
|
||||||
) throws VerificationException {
|
) throws VerificationException {
|
||||||
new SdJwtVerificationContext(sdJwtVpString, issuerSignedJWT, disclosures, keyBindingJWT.orElse(null))
|
sdJwtVerificationContext.verifyPresentation(
|
||||||
.verifyPresentation(issuerSignedJwtVerificationOpts, keyBindingJwtVerificationOpts);
|
issuerVerifyingKeys,
|
||||||
|
issuerSignedJwtVerificationOpts,
|
||||||
|
keyBindingJwtVerificationOpts,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve verification context for advanced scenarios.
|
||||||
|
*/
|
||||||
|
public SdJwtVerificationContext getSdJwtVerificationContext() {
|
||||||
|
return sdJwtVerificationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively searches the node with the given value.
|
// Recursively searches the node with the given value.
|
||||||
|
|
|
@ -23,14 +23,16 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
import org.keycloak.rule.CryptoInitRule;
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.crypto.SignatureSignerContext;
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.endsWith;
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
@ -57,38 +59,61 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
|
public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
|
||||||
for (String hashAlg : Arrays.asList(new String[]{"sha-256", "sha-384", "sha-512"})) {
|
for (String hashAlg : Arrays.asList("sha-256", "sha-384", "sha-512")) {
|
||||||
SdJwt sdJwt = exampleFlatSdJwtV1()
|
SdJwt sdJwt = exampleFlatSdJwtV1()
|
||||||
.withHashAlgorithm(hashAlg)
|
.withHashAlgorithm(hashAlg)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
|
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
|
||||||
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
|
|
||||||
|
sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException {
|
public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException {
|
||||||
SdJwt sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
SdJwt sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
|
||||||
|
sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception {
|
public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception {
|
||||||
SdJwt sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
SdJwt sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
|
||||||
|
sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_RecursiveSdJwt() throws Exception {
|
public void testSdJwtVerification_RecursiveSdJwt() throws Exception {
|
||||||
SdJwt sdJwt = exampleRecursiveSdJwtV1().build();
|
SdJwt sdJwt = exampleRecursiveSdJwtV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
|
||||||
|
sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -99,7 +124,10 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Unexpected or insecure hash algorithm: sha-224", exception.getMessage());
|
assertEquals("Unexpected or insecure hash algorithm: sha-224", exception.getMessage());
|
||||||
|
@ -110,13 +138,13 @@ public abstract class SdJwtVerificationTest {
|
||||||
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(
|
||||||
.withVerifier(testSettings.holderVerifierContext) // wrong verifier
|
Collections.singletonList(testSettings.holderVerifierContext), // wrong verifier
|
||||||
.build())
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(exception.getMessage(), is("Invalid Issuer-Signed JWT"));
|
assertThat(exception.getMessage(), is("Invalid Issuer-Signed JWT: Signature could not be verified"));
|
||||||
assertThat(exception.getCause().getMessage(), endsWith("Invalid jws signature"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -135,12 +163,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
.withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) {
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateExpirationClaim(true)
|
.withValidateExpirationClaim(true)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
|
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
|
||||||
|
@ -166,12 +197,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
||||||
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) {
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateExpirationClaim(true)
|
.withValidateExpirationClaim(true)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
|
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
|
||||||
|
@ -195,12 +229,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) {
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateIssuedAtClaim(true)
|
.withValidateIssuedAtClaim(true)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Issuer-Signed JWT: Invalid `iat` claim", exception.getMessage());
|
assertEquals("Issuer-Signed JWT: Invalid `iat` claim", exception.getMessage());
|
||||||
|
@ -224,12 +261,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
for (SdJwt sdJwt : Arrays.asList(sdJwtV1, sdJwtV2)) {
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateNotBeforeClaim(true)
|
.withValidateNotBeforeClaim(true)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Issuer-Signed JWT: Invalid `nbf` claim", exception.getMessage());
|
assertEquals("Issuer-Signed JWT: Invalid `nbf` claim", exception.getMessage());
|
||||||
|
@ -247,8 +287,10 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(
|
||||||
.build())
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Unexpected non-string element inside _sd array: 123", exception.getMessage());
|
assertEquals("Unexpected non-string element inside _sd array: 123", exception.getMessage());
|
||||||
|
@ -256,7 +298,7 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() {
|
public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() {
|
||||||
for (String forbiddenClaimName : Arrays.asList(new String[]{"_sd", "..."})) {
|
for (String forbiddenClaimName : Arrays.asList("_sd", "...")) {
|
||||||
ObjectNode claimSet = mapper.createObjectNode();
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
claimSet.put(forbiddenClaimName, "Value");
|
claimSet.put(forbiddenClaimName, "Value");
|
||||||
|
|
||||||
|
@ -266,7 +308,10 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("Disclosure claim name must not be '_sd' or '...'", exception.getMessage());
|
assertEquals("Disclosure claim name must not be '_sd' or '...'", exception.getMessage());
|
||||||
|
@ -286,7 +331,10 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertTrue(exception.getMessage().startsWith("A digest was encountered more than once:"));
|
assertTrue(exception.getMessage().startsWith("A digest was encountered more than once:"));
|
||||||
|
@ -307,15 +355,21 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts().build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals("A salt value was reused: " + salt, exception.getMessage());
|
assertEquals("A salt value was reused: " + salt, exception.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SignatureVerifierContext> defaultIssuerVerifyingKeys() {
|
||||||
|
return Collections.singletonList(testSettings.issuerVerifierContext);
|
||||||
|
}
|
||||||
|
|
||||||
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
|
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
|
||||||
return IssuerSignedJwtVerificationOpts.builder()
|
return IssuerSignedJwtVerificationOpts.builder()
|
||||||
.withVerifier(testSettings.issuerVerifierContext)
|
|
||||||
.withValidateIssuedAtClaim(false)
|
.withValidateIssuedAtClaim(false)
|
||||||
.withValidateExpirationClaim(false)
|
.withValidateExpirationClaim(false)
|
||||||
.withValidateNotBeforeClaim(false);
|
.withValidateNotBeforeClaim(false);
|
||||||
|
|
|
@ -0,0 +1,419 @@
|
||||||
|
/*
|
||||||
|
* 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.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
import org.keycloak.sdjwt.IssuerSignedJWT;
|
||||||
|
import org.keycloak.sdjwt.SdJws;
|
||||||
|
import org.keycloak.sdjwt.SdJwtUtils;
|
||||||
|
import org.keycloak.sdjwt.TestUtils;
|
||||||
|
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||||
|
|
||||||
|
import java.rmi.UnknownHostException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.endsWith;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public abstract class JwtVcMetadataTrustedSdJwtIssuerTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldResolveIssuerVerifyingKeys() throws Exception {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerUri, mockHttpDataFetcher());
|
||||||
|
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt();
|
||||||
|
List<SignatureVerifierContext> keys = trustedIssuer
|
||||||
|
.resolveIssuerVerifyingKeys(issuerSignedJWT);
|
||||||
|
|
||||||
|
// There three keys exposed on the metadata endpoint.
|
||||||
|
assertEquals(3, keys.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldResolveKeys_WhenIssuerTrustedOnRegexPattern() throws Exception {
|
||||||
|
Pattern issuerUriRegex = Pattern.compile("https://.*\\.example\\.com");
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerUriRegex, mockHttpDataFetcher());
|
||||||
|
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt();
|
||||||
|
trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldResolveKeys_WhenJwtSpecifiesKid() throws VerificationException, JsonProcessingException {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerUri, mockHttpDataFetcher());
|
||||||
|
|
||||||
|
// This JWT specifies a key ID in its header
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb--explicit-kid.txt");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<SignatureVerifierContext> keys = trustedIssuer
|
||||||
|
.resolveIssuerVerifyingKeys(issuerSignedJWT);
|
||||||
|
|
||||||
|
// Despite three keys exposed on the metadata endpoint,
|
||||||
|
// only the key matching the JWT's kid is resolved.
|
||||||
|
assertEquals(1, keys.size());
|
||||||
|
assertEquals("doc-signer-05-25-2022", keys.get(0).getKid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRejectNonHttpsIssuerURIs() {
|
||||||
|
String issuerUri = "http://issuer.example.com"; // not https
|
||||||
|
|
||||||
|
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new JwtVcMetadataTrustedSdJwtIssuer(issuerUri, mockHttpDataFetcher())
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage(), endsWith("HTTPS URI required to retrieve JWT VC Issuer Metadata"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRejectNonHttpsIssuerURIs_EvenIfIssuerBuiltWithRegexPattern() throws JsonProcessingException {
|
||||||
|
Pattern issuerUriRegex = Pattern.compile(".*\\.example\\.com");
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerUriRegex, mockHttpDataFetcher());
|
||||||
|
|
||||||
|
String issuerUri = "http://issuer.example.com"; // not https
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwtWithIssuer(issuerUri);
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage(), endsWith("HTTPS URI required to retrieve JWT VC Issuer Metadata"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRejectJwtsWithUnexpectedIssuerClaims() throws JsonProcessingException {
|
||||||
|
String regex = "https://.*\\.example\\.com";
|
||||||
|
Pattern issuerUriRegex = Pattern.compile(regex);
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerUriRegex, mockHttpDataFetcher());
|
||||||
|
|
||||||
|
String issuerUri = "https://trial.authlete.com"; // does not match the regex above
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwtWithIssuer(issuerUri);
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage(),
|
||||||
|
endsWith(String.format("Unexpected Issuer URI claim. Expected=/%s/, Got=%s", regex, issuerUri)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnInvalidJwkExposed() throws JsonProcessingException {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
metadata.set("jwks", SdJwtUtils.mapper.readTree(
|
||||||
|
"{\n" +
|
||||||
|
" \"keys\": [\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"kty\": \"EC\",\n" +
|
||||||
|
" \"x\": \"invalid\",\n" +
|
||||||
|
" \"y\": \"invalid\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
" ]\n" +
|
||||||
|
"}"
|
||||||
|
));
|
||||||
|
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockHttpDataFetcherWithMetadata(issuerUri, metadata),
|
||||||
|
"A potential JWK was retrieved but found invalid",
|
||||||
|
"Unsupported or invalid JWK"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnUnexpectedIssuerExposed() throws JsonProcessingException {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", "https://another-issuer.example.com");
|
||||||
|
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockHttpDataFetcherWithMetadata(issuerUri, metadata),
|
||||||
|
"Unexpected metadata's issuer",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnMalformedJwtVcMetadataExposed() throws JsonProcessingException {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
// This is malformed because the issuer must be a string
|
||||||
|
metadata.set("issuer", SdJwtUtils.mapper.readTree("{}"));
|
||||||
|
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockHttpDataFetcherWithMetadata(issuerUri, metadata),
|
||||||
|
"Failed to parse exposed JWT VC Metadata",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnMalformedJwksExposed() throws JsonProcessingException {
|
||||||
|
List<JsonNode> malformedJwks = Arrays.asList(
|
||||||
|
SdJwtUtils.mapper.readTree("[]"),
|
||||||
|
SdJwtUtils.mapper.readTree("{\"keys\": {}}")
|
||||||
|
);
|
||||||
|
|
||||||
|
for (JsonNode jwks : malformedJwks) {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
metadata.put("jwks_uri", "https://issuer.example.com/api/vci/jwks");
|
||||||
|
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockHttpDataFetcherWithMetadataAndJwks(issuerUri, metadata, jwks),
|
||||||
|
"Failed to parse exposed JWKS",
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnMissingJwksOnMetadataEndpoint() throws JsonProcessingException {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
|
||||||
|
// There are no means to resolve JWKS with these metadata
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockHttpDataFetcherWithMetadata(issuerUri, metadata),
|
||||||
|
String.format("Could not resolve issuer JWKs with URI: %s", issuerUri),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnEmptyPublishedJwkList() throws Exception {
|
||||||
|
// This JWKS to publish has an empty list of keys, which is unexpected.
|
||||||
|
JsonNode jwks = SdJwtUtils.mapper.readTree("{\"keys\": []}");
|
||||||
|
|
||||||
|
// JWT VC Metadata
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
metadata.set("jwks", jwks);
|
||||||
|
|
||||||
|
// Act and assert
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockHttpDataFetcherWithMetadataAndJwks(issuerUri, metadata, jwks),
|
||||||
|
String.format("Issuer JWKs were unexpectedly resolved to an empty list. Issuer URI: %s", issuerUri),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnNoPublishedJwkMatchingJwtKid() throws Exception {
|
||||||
|
// Alter kid fields of all JWKs to publish, so as none is a match
|
||||||
|
JsonNode jwks = exampleJwks();
|
||||||
|
for (JsonNode jwk : jwks.get("keys")) {
|
||||||
|
((ObjectNode) jwk).put("kid", jwk.get("kid").asText() + "-wont-match");
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT VC Metadata
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
metadata.set("jwks", jwks);
|
||||||
|
|
||||||
|
// This JWT specifies a key ID in its header
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb--explicit-kid.txt");
|
||||||
|
String kid = issuerSignedJWT.getHeader().getKeyId();
|
||||||
|
|
||||||
|
// Act and assert
|
||||||
|
genericTestShouldFail(
|
||||||
|
issuerSignedJWT,
|
||||||
|
mockHttpDataFetcherWithMetadataAndJwks(issuerUri, metadata, jwks),
|
||||||
|
String.format("No published JWK was found to match kid: %s", kid),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnPublishedJwksWithDuplicateKid() throws JsonProcessingException {
|
||||||
|
// This JWT specifies a key ID in its header
|
||||||
|
|
||||||
|
IssuerSignedJWT issuerSignedJWT = exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb--explicit-kid.txt");
|
||||||
|
|
||||||
|
// Set the same kid to all JWKs to publish, which is problematic
|
||||||
|
|
||||||
|
String kid = issuerSignedJWT.getHeader().getKeyId();
|
||||||
|
JsonNode jwks = exampleJwks();
|
||||||
|
for (JsonNode jwk : jwks.get("keys")) {
|
||||||
|
((ObjectNode) jwk).put("kid", kid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT VC Metadata
|
||||||
|
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
metadata.set("jwks", jwks);
|
||||||
|
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerUri, mockHttpDataFetcherWithMetadata(issuerUri, metadata));
|
||||||
|
|
||||||
|
// Act and assert
|
||||||
|
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(exception.getMessage(),
|
||||||
|
endsWith(String.format("Cannot choose between multiple exposed JWKs with same kid: %s", kid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailOnIOErrorWhileFetchingMetadata() throws JsonProcessingException {
|
||||||
|
String issuerUri = "https://issuer.example.com";
|
||||||
|
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", issuerUri);
|
||||||
|
|
||||||
|
HttpDataFetcher mockFetcher = mockHttpDataFetcherWithMetadata(
|
||||||
|
// HTTP can only mock an unrelated issuer
|
||||||
|
"https://another-issuer.example.com",
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
genericTestShouldFail(
|
||||||
|
exampleIssuerSignedJwt(),
|
||||||
|
mockFetcher,
|
||||||
|
String.format("Could not fetch data from URI: %s", issuerUri),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void genericTestShouldFail(
|
||||||
|
IssuerSignedJWT issuerSignedJWT,
|
||||||
|
HttpDataFetcher mockFetcher,
|
||||||
|
String errorMessage,
|
||||||
|
String causeErrorMessage
|
||||||
|
) {
|
||||||
|
TrustedSdJwtIssuer trustedIssuer = new JwtVcMetadataTrustedSdJwtIssuer(
|
||||||
|
issuerSignedJWT.getPayload().get(SdJws.CLAIM_NAME_ISSUER).asText(),
|
||||||
|
mockFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> trustedIssuer.resolveIssuerVerifyingKeys(issuerSignedJWT)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains(errorMessage));
|
||||||
|
if (causeErrorMessage != null) {
|
||||||
|
assertTrue(exception.getCause().getMessage().contains(causeErrorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJWT exampleIssuerSignedJwt() {
|
||||||
|
return exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJWT exampleIssuerSignedJwt(String sdJwtVector) {
|
||||||
|
return exampleIssuerSignedJwt(sdJwtVector, "https://issuer.example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJWT exampleIssuerSignedJwtWithIssuer(String issuerUri) {
|
||||||
|
return exampleIssuerSignedJwt("sdjwt/s20.1-sdjwt+kb.txt", issuerUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJWT exampleIssuerSignedJwt(String sdJwtVector, String issuerUri) {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), sdJwtVector);
|
||||||
|
IssuerSignedJWT issuerSignedJWT = SdJwtVP.of(sdJwtVPString).getIssuerSignedJWT();
|
||||||
|
((ObjectNode) issuerSignedJWT.getPayload()).put("iss", issuerUri);
|
||||||
|
return issuerSignedJWT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode exampleJwks() throws JsonProcessingException {
|
||||||
|
return SdJwtUtils.mapper.readTree(
|
||||||
|
TestUtils.readFileAsString(getClass(),
|
||||||
|
"sdjwt/s30.1-jwt-vc-metadata-jwks.json"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpDataFetcher mockHttpDataFetcher() throws JsonProcessingException {
|
||||||
|
ObjectNode metadata = SdJwtUtils.mapper.createObjectNode();
|
||||||
|
metadata.put("issuer", "https://issuer.example.com");
|
||||||
|
metadata.put("jwks_uri", "https://issuer.example.com/api/vci/jwks");
|
||||||
|
|
||||||
|
return mockHttpDataFetcherWithMetadata(
|
||||||
|
metadata.get("issuer").asText(),
|
||||||
|
metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpDataFetcher mockHttpDataFetcherWithMetadata(
|
||||||
|
String issuer, JsonNode metadata
|
||||||
|
) throws JsonProcessingException {
|
||||||
|
return mockHttpDataFetcherWithMetadataAndJwks(issuer, metadata, exampleJwks());
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpDataFetcher mockHttpDataFetcherWithMetadataAndJwks(
|
||||||
|
String issuer, JsonNode metadata, JsonNode jwks
|
||||||
|
) {
|
||||||
|
return uri -> {
|
||||||
|
if (!uri.startsWith(issuer)) {
|
||||||
|
throw new UnknownHostException("Unavailable URI");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.endsWith("/.well-known/jwt-vc-issuer")) {
|
||||||
|
return metadata;
|
||||||
|
} else if (uri.endsWith("/api/vci/jwks")) {
|
||||||
|
return jwks;
|
||||||
|
} else {
|
||||||
|
throw new UnknownHostException("Unavailable URI");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* 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.consumer;
|
||||||
|
|
||||||
|
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.TestSettings;
|
||||||
|
import org.keycloak.sdjwt.TestUtils;
|
||||||
|
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
|
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public abstract class SdJwtPresentationConsumerTest {
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
||||||
|
SdJwtPresentationConsumer sdJwtPresentationConsumer = new SdJwtPresentationConsumer();
|
||||||
|
static TestSettings testSettings = TestSettings.getInstance();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldVerifySdJwtPresentation() throws VerificationException {
|
||||||
|
sdJwtPresentationConsumer.verifySdJwtPresentation(
|
||||||
|
exampleSdJwtVP(),
|
||||||
|
examplePresentationRequirements(),
|
||||||
|
exampleTrustedSdJwtIssuers(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFail_IfPresentationRequirementsNotMet() {
|
||||||
|
SimplePresentationDefinition definition = SimplePresentationDefinition.builder()
|
||||||
|
.addClaimRequirement("vct", ".*diploma.*")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> sdJwtPresentationConsumer.verifySdJwtPresentation(
|
||||||
|
exampleSdJwtVP(),
|
||||||
|
definition,
|
||||||
|
exampleTrustedSdJwtIssuers(),
|
||||||
|
defaultIssuerSignedJwtVerificationOpts(),
|
||||||
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage()
|
||||||
|
.contains("A required field was not presented: `vct`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SdJwtVP exampleSdJwtVP() {
|
||||||
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
|
||||||
|
return SdJwtVP.of(sdJwtVPString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PresentationRequirements examplePresentationRequirements() {
|
||||||
|
return SimplePresentationDefinition.builder()
|
||||||
|
.addClaimRequirement("sub", "\"user_[0-9]+\"")
|
||||||
|
.addClaimRequirement("given_name", ".*")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TrustedSdJwtIssuer> exampleTrustedSdJwtIssuers() {
|
||||||
|
return Arrays.asList(
|
||||||
|
new StaticTrustedSdJwtIssuer(
|
||||||
|
Collections.singletonList(testSettings.holderVerifierContext)
|
||||||
|
),
|
||||||
|
new StaticTrustedSdJwtIssuer(
|
||||||
|
Collections.singletonList(testSettings.issuerVerifierContext)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IssuerSignedJwtVerificationOpts defaultIssuerSignedJwtVerificationOpts() {
|
||||||
|
return IssuerSignedJwtVerificationOpts.builder()
|
||||||
|
.withValidateIssuedAtClaim(false)
|
||||||
|
.withValidateNotBeforeClaim(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyBindingJwtVerificationOpts defaultKeyBindingJwtVerificationOpts() {
|
||||||
|
return KeyBindingJwtVerificationOpts.builder()
|
||||||
|
.withKeyBindingRequired(true)
|
||||||
|
.withAllowedMaxAge(Integer.MAX_VALUE)
|
||||||
|
.withNonce("1234567890")
|
||||||
|
.withAud("https://verifier.example.org")
|
||||||
|
.withValidateExpirationClaim(false)
|
||||||
|
.withValidateNotBeforeClaim(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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.consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.sdjwt.SdJwtUtils;
|
||||||
|
import org.keycloak.sdjwt.TestUtils;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.startsWith;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class SimplePresentationDefinitionTest {
|
||||||
|
|
||||||
|
private final ObjectMapper mapper = SdJwtUtils.mapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCheckIfSatisfiedBy() throws VerificationException, JsonProcessingException {
|
||||||
|
SimplePresentationDefinition definition = SimplePresentationDefinition.builder()
|
||||||
|
.addClaimRequirement("vct", ".*identity_credential.*")
|
||||||
|
.addClaimRequirement("given_name", "\"John\"")
|
||||||
|
.addClaimRequirement("cat", "123")
|
||||||
|
.addClaimRequirement("addr", ".*\"(Douala|Berlin)\".*")
|
||||||
|
.addClaimRequirement("colors", "\\[\"red\",.*")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
definition.checkIfSatisfiedBy(exampleDisclosedPayload());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCheckIfSatisfiedBy_shouldFailOnRequiredFieldMissing() {
|
||||||
|
SimplePresentationDefinition definition = SimplePresentationDefinition.builder()
|
||||||
|
.addClaimRequirement("family_name", ".*")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> definition.checkIfSatisfiedBy(exampleDisclosedPayload()));
|
||||||
|
|
||||||
|
assertEquals("A required field was not presented: `family_name`", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCheckIfSatisfiedBy_shouldFailOnNonMatchingPattern() {
|
||||||
|
SimplePresentationDefinition definition = SimplePresentationDefinition.builder()
|
||||||
|
.addClaimRequirement("vct", ".*diploma.*")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
VerificationException exception = assertThrows(VerificationException.class,
|
||||||
|
() -> definition.checkIfSatisfiedBy(exampleDisclosedPayload()));
|
||||||
|
|
||||||
|
assertThat(exception.getMessage(), startsWith("Pattern matching failed for required field"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode exampleDisclosedPayload() throws JsonProcessingException {
|
||||||
|
String content = TestUtils.readFileAsString(getClass(),
|
||||||
|
"sdjwt/s7.4-sample-disclosed-issuer-payload.json");
|
||||||
|
return mapper.readTree(content);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.crypto.SignatureVerifierContext;
|
||||||
import org.keycloak.rule.CryptoInitRule;
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts;
|
||||||
import org.keycloak.sdjwt.SdJwt;
|
import org.keycloak.sdjwt.SdJwt;
|
||||||
|
@ -34,6 +35,7 @@ import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
@ -63,6 +65,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
sdJwtVP.verify(
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
defaultKeyBindingJwtVerificationOpts().build()
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
);
|
);
|
||||||
|
@ -70,15 +73,17 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerif_s20_8_sdjwt_with_kb__AltCnfCurves() throws VerificationException {
|
public void testVerif_s20_8_sdjwt_with_kb__AltCnfCurves() throws VerificationException {
|
||||||
List<String> entries = Arrays.asList(new String[]{
|
List<String> entries = Arrays.asList(
|
||||||
"sdjwt/s20.8-sdjwt+kb--es384.txt", "sdjwt/s20.8-sdjwt+kb--es512.txt"
|
"sdjwt/s20.8-sdjwt+kb--es384.txt",
|
||||||
});
|
"sdjwt/s20.8-sdjwt+kb--es512.txt"
|
||||||
|
);
|
||||||
|
|
||||||
for (String entry : entries) {
|
for (String entry : entries) {
|
||||||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
||||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
sdJwtVP.verify(
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
defaultKeyBindingJwtVerificationOpts().build()
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
);
|
);
|
||||||
|
@ -87,18 +92,19 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerif_s20_8_sdjwt_with_kb__CnfRSA() throws VerificationException {
|
public void testVerif_s20_8_sdjwt_with_kb__CnfRSA() throws VerificationException {
|
||||||
List<String> entries = Arrays.asList(new String[]{
|
List<String> entries = Arrays.asList(
|
||||||
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-rs256.txt",
|
"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-ps256.txt",
|
||||||
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt",
|
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt",
|
||||||
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt"
|
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt"
|
||||||
});
|
);
|
||||||
|
|
||||||
for (String entry : entries) {
|
for (String entry : entries) {
|
||||||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
||||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
sdJwtVP.verify(
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
defaultKeyBindingJwtVerificationOpts().build()
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
);
|
);
|
||||||
|
@ -111,6 +117,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
sdJwtVP.verify(
|
sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
defaultKeyBindingJwtVerificationOpts()
|
defaultKeyBindingJwtVerificationOpts()
|
||||||
.withKeyBindingRequired(false)
|
.withKeyBindingRequired(false)
|
||||||
|
@ -327,6 +334,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
UnsupportedOperationException exception = assertThrows(
|
UnsupportedOperationException exception = assertThrows(
|
||||||
UnsupportedOperationException.class,
|
UnsupportedOperationException.class,
|
||||||
() -> sdJwtVP.verify(
|
() -> sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
defaultKeyBindingJwtVerificationOpts().build()
|
defaultKeyBindingJwtVerificationOpts().build()
|
||||||
)
|
)
|
||||||
|
@ -341,8 +349,8 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
// The cnf/jwk object has an unrecognized key type
|
// The cnf/jwk object has an unrecognized key type
|
||||||
"sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt",
|
"sdjwt/s20.8-sdjwt+kb--cnf-jwk-is-malformed.txt",
|
||||||
defaultKeyBindingJwtVerificationOpts().build(),
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
"Malformed or unsupported cnf/jwk claim",
|
"Could not process cnf/jwk",
|
||||||
null
|
"Unsupported or invalid JWK"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,8 +360,8 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
// HMAC cnf/jwk parsing is not supported
|
// HMAC cnf/jwk parsing is not supported
|
||||||
"sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt",
|
"sdjwt/s20.8-sdjwt+kb--cnf-hmac.txt",
|
||||||
defaultKeyBindingJwtVerificationOpts().build(),
|
defaultKeyBindingJwtVerificationOpts().build(),
|
||||||
"Malformed or unsupported cnf/jwk claim",
|
"Could not process cnf/jwk",
|
||||||
null
|
"Unsupported or invalid JWK"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,6 +377,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwtVP.verify(
|
() -> sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
keyBindingJwtVerificationOpts
|
keyBindingJwtVerificationOpts
|
||||||
)
|
)
|
||||||
|
@ -405,6 +414,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
VerificationException exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwtVP.verify(
|
() -> sdJwtVP.verify(
|
||||||
|
defaultIssuerVerifyingKeys(),
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
keyBindingJwtVerificationOpts
|
keyBindingJwtVerificationOpts
|
||||||
)
|
)
|
||||||
|
@ -416,9 +426,12 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SignatureVerifierContext> defaultIssuerVerifyingKeys() {
|
||||||
|
return Collections.singletonList(testSettings.issuerVerifierContext);
|
||||||
|
}
|
||||||
|
|
||||||
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
|
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
|
||||||
return IssuerSignedJwtVerificationOpts.builder()
|
return IssuerSignedJwtVerificationOpts.builder()
|
||||||
.withVerifier(testSettings.issuerVerifierContext)
|
|
||||||
.withValidateIssuedAtClaim(false)
|
.withValidateIssuedAtClaim(false)
|
||||||
.withValidateNotBeforeClaim(false);
|
.withValidateNotBeforeClaim(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
eyJhbGciOiJFUzI1NiIsInR5cCI6ImV4YW1wbGUrc2Qtand0Iiwia2lkIjoiZG9jLXNpZ25lci0wNS0yNS0yMDIyIn0.eyJfc2QiOlsiQ3JRZTdTNWtxQkFIdC1uTVlYZ2M2YmR0MlNINWFUWTFzVV9NLVBna2pQSSIsIkp6WWpINHN2bGlIMFIzUHlFTWZlWnU2SnQ2OXU1cWVoWm83RjdFUFlsU0UiLCJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwiVEdmNG9MYmd3ZDVKUWFIeUtWUVpVOVVkR0UwdzVydERzclp6ZlVhb21MbyIsIlhRXzNrUEt0MVh5WDdLQU5rcVZSNnlaMlZhNU5yUEl2UFlieU12UktCTU0iLCJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiXSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMCwic3ViIjoidXNlcl80MiIsIm5hdGlvbmFsaXRpZXMiOlt7Ii4uLiI6InBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8ifSx7Ii4uLiI6IjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCJ5IjoiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.N0xUjkyxK6q-uvDF0bLpOSq8XI-QXZ9iI5U4w4GSx9NwDZQfg4P9SffgjQ11LwZKKfLprNernp53-oRBWaOuDA
|
||||||
|
|
||||||
|
~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0
|
||||||
|
~
|
||||||
|
|
||||||
|
eyJraWQiOiJob2xkZXIiLCJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAiLCJhdWQiOiJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwiaWF0IjoxNzAyMzE1Njc5LCJzZF9oYXNoIjoiSEtwV1JKMmtqaXluSjBBbDlNTFJ5dmFjSS1HSVpmMHN5SUVvUnB2VktESSJ9.YFBWGvdAq8UIz7Y3b2lVMaQAFCkS02qkClGOPsn9qE-xDOgqT6VYx2D9-nSAU69dvkTdq6ynPMutlCYNtvtZ6w
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"kid": "doc-signer-05-25-2022",
|
||||||
|
"kty": "EC",
|
||||||
|
"d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ",
|
||||||
|
"y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"kid": "J1FwJP87C6-QN_WSIOmJAQc6n5CQ_bZdaFJ5GDnW1Rk",
|
||||||
|
"x5c": [
|
||||||
|
"MIIBXTCCAQSgAwIBAgIGAYyR2cIZMAoGCCqGSM49BAMCMDYxNDAyBgNVBAMMK0oxRndKUDg3QzYtUU5fV1NJT21KQVFjNm41Q1FfYlpkYUZKNUdEblcxUmswHhcNMjMxMjIyMTQwNjU2WhcNMjQxMDE3MTQwNjU2WjA2MTQwMgYDVQQDDCtKMUZ3SlA4N0M2LVFOX1dTSU9tSkFRYzZuNUNRX2JaZGFGSjVHRG5XMVJrMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAopVeboJpYRycw1YKkkROXfCpKEKl9Y1YPFhOGj4xTg2UOunxTxSIVkT94qFVIuu1hkEoE2NxelZo3+yTFUODDAKBggqhkjOPQQDAgNHADBEAiBnFjScBcvERleLjMCu5NbxJKkNsa/gQhkXTfDmbq+T3gIgVazbsVdQvZgluc9nJYQxWlzXT9i6f+wgUKx0KCYbj3A="
|
||||||
|
],
|
||||||
|
"x": "AopVeboJpYRycw1YKkkROXfCpKEKl9Y1YPFhOGj4xTg",
|
||||||
|
"y": "NlDrp8U8UiFZE_eKhVSLrtYZBKBNjcXpWaN_skxVDgw",
|
||||||
|
"alg": "ES256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"kid": "ZbAAKwhynrqBnYlHdEkBIvNJFZZH_bRg1KIopKfZ6O8",
|
||||||
|
"x": "eshMYyyoEsH_Eb85a7o76msXFPokfvNaeyY3u5qDm3M",
|
||||||
|
"y": "q0lGMn_UXiWJdgJtSCNzh9zPC6s7qKqQMo4V1i-69jA",
|
||||||
|
"alg": "ES256"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"vct": "https://credentials.example.com/identity_credential",
|
||||||
|
"given_name": "John",
|
||||||
|
"cat": 123,
|
||||||
|
"addr": {"city": "Douala", "country": "CM"},
|
||||||
|
"colors": ["red", "green"]
|
||||||
|
}
|
|
@ -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.consumer.JwtVcMetadataTrustedSdJwtIssuerTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class DefaultCryptoJwtVcMetadataTrustedSdJwtIssuerTest extends JwtVcMetadataTrustedSdJwtIssuerTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
// Run this test just if java is not in FIPS mode
|
||||||
|
Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.crypto.def.test.sdjwt;
|
||||||
|
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.keycloak.common.util.Environment;
|
||||||
|
import org.keycloak.sdjwt.consumer.SdJwtPresentationConsumerTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class DefaultCryptoSdJwtPresentationConsumerTest extends SdJwtPresentationConsumerTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void before() {
|
||||||
|
// Run this test just if java is not in FIPS mode
|
||||||
|
Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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.consumer.JwtVcMetadataTrustedSdJwtIssuerTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class ElytronCryptoJwtVcMetadataTrustedSdJwtIssuerTest extends JwtVcMetadataTrustedSdJwtIssuerTest {
|
||||||
|
}
|
|
@ -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.consumer.SdJwtPresentationConsumerTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:Ingrid.Kamga@adorsys.com">Ingrid Kamga</a>
|
||||||
|
*/
|
||||||
|
public class ElytronCryptoSdJwtPresentationConsumerTest extends SdJwtPresentationConsumerTest {
|
||||||
|
}
|
Loading…
Reference in a new issue