Downgrade Java for client libraries to 8
Closes #33051 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
71bc313297
commit
c532751ff4
19 changed files with 408 additions and 240 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -85,7 +85,7 @@ jobs:
|
||||||
SEP=","
|
SEP=","
|
||||||
done
|
done
|
||||||
|
|
||||||
./mvnw test -pl "$PROJECTS" -am
|
./mvnw install -pl "$PROJECTS" -am
|
||||||
|
|
||||||
- name: Upload JVM Heapdumps
|
- name: Upload JVM Heapdumps
|
||||||
if: always()
|
if: always()
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
<description>Keycloak Authz: Client API. This module is supposed to be used just in the Keycloak repository for the testsuite. It is NOT supposed to be used by the 3rd party applications.
|
<description>Keycloak Authz: Client API. This module is supposed to be used just in the Keycloak repository for the testsuite. It is NOT supposed to be used by the 3rd party applications.
|
||||||
For the use by 3rd party applications, please use `org.keycloak:keycloak-authz-client` module.</description>
|
For the use by 3rd party applications, please use `org.keycloak:keycloak-authz-client` module.</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<maven.compiler.release>8</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
|
@ -32,8 +32,9 @@
|
||||||
<description>Common library and dependencies shared with server and all adapters</description>
|
<description>Common library and dependencies shared with server and all adapters</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- We still need to support EAP 8, set the Java version to 11. -->
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
<maven.compiler.release>11</maven.compiler.release>
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<maven.compiler.release>8</maven.compiler.release>
|
||||||
<timestamp>${maven.build.timestamp}</timestamp>
|
<timestamp>${maven.build.timestamp}</timestamp>
|
||||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
|
@ -988,7 +988,7 @@ public class Reflections {
|
||||||
* @throws InstantiationException
|
* @throws InstantiationException
|
||||||
* @deprecated for removal in Keycloak 27
|
* @deprecated for removal in Keycloak 27
|
||||||
*/
|
*/
|
||||||
@Deprecated(forRemoval = true)
|
@Deprecated
|
||||||
public static <T> T newInstance(final Class<T> fromClass) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
public static <T> T newInstance(final Class<T> fromClass) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||||
return newInstance(fromClass, fromClass.getName());
|
return newInstance(fromClass, fromClass.getName());
|
||||||
}
|
}
|
||||||
|
@ -1008,7 +1008,7 @@ public class Reflections {
|
||||||
* @throws InstantiationException
|
* @throws InstantiationException
|
||||||
* @deprecated for removal in Keycloak 27
|
* @deprecated for removal in Keycloak 27
|
||||||
*/
|
*/
|
||||||
@Deprecated(forRemoval = true)
|
@Deprecated
|
||||||
public static <T> T newInstance(final Class<?> type, final String fullQualifiedName) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
public static <T> T newInstance(final Class<?> type, final String fullQualifiedName) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||||
return (T) classForName(fullQualifiedName, type.getClassLoader()).newInstance();
|
return (T) classForName(fullQualifiedName, type.getClassLoader()).newInstance();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import java.security.PrivilegedAction;
|
||||||
* A {@link java.security.PrivilegedAction} that calls {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)}
|
* A {@link java.security.PrivilegedAction} that calls {@link java.lang.reflect.AccessibleObject#setAccessible(boolean)}
|
||||||
* @deprecated for removal in Keycloak 27
|
* @deprecated for removal in Keycloak 27
|
||||||
*/
|
*/
|
||||||
@Deprecated(forRemoval = true)
|
@Deprecated
|
||||||
public class SetAccessiblePrivilegedAction implements PrivilegedAction<Void> {
|
public class SetAccessiblePrivilegedAction implements PrivilegedAction<Void> {
|
||||||
|
|
||||||
private final AccessibleObject member;
|
private final AccessibleObject member;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import java.security.PrivilegedAction;
|
||||||
* A {@link PrivilegedAction} that calls {@link AccessibleObject#setAccessible(boolean)}
|
* A {@link PrivilegedAction} that calls {@link AccessibleObject#setAccessible(boolean)}
|
||||||
* @deprecated for removal in Keycloak 27
|
* @deprecated for removal in Keycloak 27
|
||||||
*/
|
*/
|
||||||
@Deprecated(forRemoval = true)
|
@Deprecated
|
||||||
public class UnSetAccessiblePrivilegedAction implements PrivilegedAction<Void> {
|
public class UnSetAccessiblePrivilegedAction implements PrivilegedAction<Void> {
|
||||||
|
|
||||||
private final AccessibleObject member;
|
private final AccessibleObject member;
|
||||||
|
|
35
core/pom.xml
35
core/pom.xml
|
@ -32,6 +32,9 @@
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<maven.compiler.release>8</maven.compiler.release>
|
||||||
<timestamp>${maven.build.timestamp}</timestamp>
|
<timestamp>${maven.build.timestamp}</timestamp>
|
||||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||||
</properties>
|
</properties>
|
||||||
|
@ -72,6 +75,38 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>jdk-16</id>
|
||||||
|
<activation>
|
||||||
|
<jdk>[16,)</jdk>
|
||||||
|
</activation>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>compile-java16</id>
|
||||||
|
<phase>compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<release>16</release>
|
||||||
|
<compileSourceRoots>
|
||||||
|
<compileSourceRoot>${project.basedir}/src/main/java16</compileSourceRoot>
|
||||||
|
</compileSourceRoots>
|
||||||
|
<multiReleaseOutput>true</multiReleaseOutput>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
|
|
|
@ -17,19 +17,10 @@
|
||||||
|
|
||||||
package org.keycloak.jose.jwk;
|
package org.keycloak.jose.jwk;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.security.interfaces.EdECPublicKey;
|
|
||||||
import java.security.spec.EdECPoint;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
|
||||||
import org.keycloak.common.util.KeyUtils;
|
|
||||||
import org.keycloak.crypto.Algorithm;
|
|
||||||
import org.keycloak.crypto.KeyType;
|
|
||||||
import org.keycloak.crypto.KeyUse;
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -54,55 +45,13 @@ public class JWKBuilder extends AbstractJWKBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JWK okp(Key key) {
|
public JWK okp(Key key) {
|
||||||
return okp(key, DEFAULT_PUBLIC_KEY_USE);
|
// not supported if jdk vesion < 17
|
||||||
|
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JWK okp(Key key, KeyUse keyUse) {
|
public JWK okp(Key key, KeyUse keyUse) {
|
||||||
EdECPublicKey eddsaPublicKey = (EdECPublicKey) key;
|
// not supported if jdk version < 17
|
||||||
|
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
||||||
OKPPublicJWK k = new OKPPublicJWK();
|
|
||||||
|
|
||||||
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
|
||||||
|
|
||||||
k.setKeyId(kid);
|
|
||||||
k.setKeyType(KeyType.OKP);
|
|
||||||
k.setAlgorithm(algorithm);
|
|
||||||
k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
|
|
||||||
k.setCrv(eddsaPublicKey.getParams().getName());
|
|
||||||
|
|
||||||
Optional<String> x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
|
|
||||||
k.setX(x.orElse(""));
|
|
||||||
|
|
||||||
return k;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<String> edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) {
|
|
||||||
EdECPoint edEcPoint = eddsaPublicKey.getPoint();
|
|
||||||
BigInteger yCoordinate = edEcPoint.getY();
|
|
||||||
|
|
||||||
// JWK representation "x" of a public key
|
|
||||||
int bytesLength = 0;
|
|
||||||
if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) {
|
|
||||||
bytesLength = 32;
|
|
||||||
} else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) {
|
|
||||||
bytesLength = 57;
|
|
||||||
} else {
|
|
||||||
return Optional.ofNullable(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate.
|
|
||||||
byte[] yCoordinateLittleEndianBytes = new byte[bytesLength];
|
|
||||||
|
|
||||||
// convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027)
|
|
||||||
yCoordinateLittleEndianBytes = Arrays.copyOf(reverseBytes(yCoordinate.toByteArray()), bytesLength);
|
|
||||||
|
|
||||||
// set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037)
|
|
||||||
if (edEcPoint.isXOdd()) {
|
|
||||||
yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,18 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.jose.jwk;
|
package org.keycloak.jose.jwk;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.spec.EdECPoint;
|
|
||||||
import java.security.spec.EdECPublicKeySpec;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.NamedParameterSpec;
|
|
||||||
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
|
||||||
import org.keycloak.crypto.Algorithm;
|
|
||||||
import org.keycloak.crypto.KeyType;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,68 +48,4 @@ public class JWKParser extends AbstractJWKParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public PublicKey toPublicKey() {
|
|
||||||
if (jwk == null) {
|
|
||||||
throw new IllegalStateException("Not possible to convert to the publicKey. The jwk is not set");
|
|
||||||
}
|
|
||||||
String keyType = jwk.getKeyType();
|
|
||||||
if (KeyType.RSA.equals(keyType)) {
|
|
||||||
return createRSAPublicKey();
|
|
||||||
} else if (KeyType.EC.equals(keyType)) {
|
|
||||||
return createECPublicKey();
|
|
||||||
} else if (KeyType.OKP.equals(keyType)) {
|
|
||||||
return createOKPPublicKey();
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Unsupported keyType " + keyType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublicKey createOKPPublicKey() {
|
|
||||||
String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X);
|
|
||||||
String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV);
|
|
||||||
// JWK representation "x" of a public key
|
|
||||||
int bytesLength = 0;
|
|
||||||
if (Algorithm.Ed25519.equals(crv)) {
|
|
||||||
bytesLength = 32;
|
|
||||||
} else if (Algorithm.Ed448.equals(crv)) {
|
|
||||||
bytesLength = 57;
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Invalid JWK representation of OKP type algorithm");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] decodedX = Base64Url.decode(x);
|
|
||||||
if (decodedX.length != bytesLength) {
|
|
||||||
throw new RuntimeException("Invalid JWK representation of OKP type public key");
|
|
||||||
}
|
|
||||||
|
|
||||||
// x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even
|
|
||||||
boolean isOddX = false;
|
|
||||||
if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000
|
|
||||||
isOddX = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0
|
|
||||||
decodedX[decodedX.length - 1] &= 127; // 0b01111111
|
|
||||||
|
|
||||||
// both x and y-coordinate in twisted Edwards curve are always 0 or natural number
|
|
||||||
BigInteger y = new BigInteger(1, JWKBuilder.reverseBytes(decodedX));
|
|
||||||
NamedParameterSpec spec = new NamedParameterSpec(crv);
|
|
||||||
EdECPoint ep = new EdECPoint(isOddX, y);
|
|
||||||
EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep);
|
|
||||||
|
|
||||||
PublicKey publicKey = null;
|
|
||||||
try {
|
|
||||||
publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec);
|
|
||||||
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isKeyTypeSupported(String keyType) {
|
|
||||||
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) || OKPPublicJWK.OKP.equals(keyType));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.sdjwt;
|
package org.keycloak.sdjwt;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -140,7 +142,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
* Returns `cnf` claim (establishing key binding)
|
* Returns `cnf` claim (establishing key binding)
|
||||||
*/
|
*/
|
||||||
public Optional<JsonNode> getCnfClaim() {
|
public Optional<JsonNode> getCnfClaim() {
|
||||||
var cnf = getPayload().get("cnf");
|
JsonNode cnf = getPayload().get("cnf");
|
||||||
return Optional.ofNullable(cnf);
|
return Optional.ofNullable(cnf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +150,7 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
* Returns declared hash algorithm from SD hash claim.
|
* Returns declared hash algorithm from SD hash claim.
|
||||||
*/
|
*/
|
||||||
public String getSdHashAlg() {
|
public String getSdHashAlg() {
|
||||||
var hashAlgNode = getPayload().get(CLAIM_NAME_SD_HASH_ALGORITHM);
|
JsonNode hashAlgNode = getPayload().get(CLAIM_NAME_SD_HASH_ALGORITHM);
|
||||||
return hashAlgNode == null ? "sha-256" : hashAlgNode.asText();
|
return hashAlgNode == null ? "sha-256" : hashAlgNode.asText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,10 +161,10 @@ public class IssuerSignedJWT extends SdJws {
|
||||||
*/
|
*/
|
||||||
public void verifySdHashAlgorithm() throws VerificationException {
|
public void verifySdHashAlgorithm() throws VerificationException {
|
||||||
// Known secure algorithms
|
// Known secure algorithms
|
||||||
final Set<String> secureAlgorithms = Set.of(
|
final Set<String> secureAlgorithms = new HashSet<>(Arrays.asList(
|
||||||
"sha-256", "sha-384", "sha-512",
|
"sha-256", "sha-384", "sha-512",
|
||||||
"sha3-256", "sha3-384", "sha3-512"
|
"sha3-256", "sha3-384", "sha3-512"
|
||||||
);
|
));
|
||||||
|
|
||||||
// Read SD hash claim
|
// Read SD hash claim
|
||||||
String hashAlg = getSdHashAlg();
|
String hashAlg = getSdHashAlg();
|
||||||
|
|
|
@ -83,7 +83,7 @@ public class SdJwtUtils {
|
||||||
JsonNode jsonNode;
|
JsonNode jsonNode;
|
||||||
|
|
||||||
// Decode Base64URL-encoded disclosure
|
// Decode Base64URL-encoded disclosure
|
||||||
var decoded = new String(decodeNoPad(disclosure));
|
String decoded = new String(decodeNoPad(disclosure));
|
||||||
|
|
||||||
// Parse the disclosure string into a JSON array
|
// Parse the disclosure string into a JSON array
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -32,7 +32,9 @@ import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
import org.keycloak.util.JWKSUtils;
|
import org.keycloak.util.JWKSUtils;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -75,9 +77,9 @@ public class SdJwtVerificationContext {
|
||||||
private Map<String, String> computeDigestDisclosureMap(List<String> disclosureStrings) {
|
private Map<String, String> computeDigestDisclosureMap(List<String> disclosureStrings) {
|
||||||
return disclosureStrings.stream()
|
return disclosureStrings.stream()
|
||||||
.map(disclosureString -> {
|
.map(disclosureString -> {
|
||||||
var digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
String digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
||||||
disclosureString.getBytes(), issuerSignedJwt.getSdHashAlg());
|
disclosureString.getBytes(), issuerSignedJwt.getSdHashAlg());
|
||||||
return Map.entry(digest, disclosureString);
|
return new AbstractMap.SimpleEntry<String,String>(digest, disclosureString);
|
||||||
})
|
})
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
}
|
}
|
||||||
|
@ -103,7 +105,7 @@ public class SdJwtVerificationContext {
|
||||||
validateIssuerSignedJwt(issuerSignedJwtVerificationOpts.getVerifier());
|
validateIssuerSignedJwt(issuerSignedJwtVerificationOpts.getVerifier());
|
||||||
|
|
||||||
// Validate disclosures.
|
// Validate disclosures.
|
||||||
var disclosedPayload = validateDisclosuresDigests();
|
JsonNode disclosedPayload = validateDisclosuresDigests();
|
||||||
|
|
||||||
// Validate time claims.
|
// Validate time claims.
|
||||||
// Issuers will typically include claims controlling the validity of the SD-JWT in plaintext in the
|
// Issuers will typically include claims controlling the validity of the SD-JWT in plaintext in the
|
||||||
|
@ -182,13 +184,13 @@ public class SdJwtVerificationContext {
|
||||||
validateKeyBindingJwtTyp();
|
validateKeyBindingJwtTyp();
|
||||||
|
|
||||||
// Determine the public key for the Holder from the SD-JWT
|
// Determine the public key for the Holder from the SD-JWT
|
||||||
var cnf = issuerSignedJwt.getCnfClaim().orElseThrow(
|
JsonNode cnf = issuerSignedJwt.getCnfClaim().orElseThrow(
|
||||||
() -> new VerificationException("No cnf claim in Issuer-signed JWT for key binding")
|
() -> new VerificationException("No cnf claim in Issuer-signed JWT for key binding")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure that a signing algorithm was used that was deemed secure for the application.
|
// Ensure that a signing algorithm was used that was deemed secure for the application.
|
||||||
// The none algorithm MUST NOT be accepted.
|
// The none algorithm MUST NOT be accepted.
|
||||||
var holderVerifier = buildHolderVerifier(cnf);
|
SignatureVerifierContext holderVerifier = buildHolderVerifier(cnf);
|
||||||
|
|
||||||
// Validate the signature over the Key Binding JWT
|
// Validate the signature over the Key Binding JWT
|
||||||
try {
|
try {
|
||||||
|
@ -219,7 +221,7 @@ public class SdJwtVerificationContext {
|
||||||
* @throws VerificationException if verification failed
|
* @throws VerificationException if verification failed
|
||||||
*/
|
*/
|
||||||
private void validateKeyBindingJwtTyp() throws VerificationException {
|
private void validateKeyBindingJwtTyp() throws VerificationException {
|
||||||
var typ = keyBindingJwt.getHeader().getType();
|
String typ = keyBindingJwt.getHeader().getType();
|
||||||
if (!typ.equals(KeyBindingJWT.TYP)) {
|
if (!typ.equals(KeyBindingJWT.TYP)) {
|
||||||
throw new VerificationException("Key Binding JWT is not of declared typ " + KeyBindingJWT.TYP);
|
throw new VerificationException("Key Binding JWT is not of declared typ " + KeyBindingJWT.TYP);
|
||||||
}
|
}
|
||||||
|
@ -234,7 +236,7 @@ public class SdJwtVerificationContext {
|
||||||
Objects.requireNonNull(cnf);
|
Objects.requireNonNull(cnf);
|
||||||
|
|
||||||
// Read JWK
|
// Read JWK
|
||||||
var cnfJwk = cnf.get("jwk");
|
JsonNode cnfJwk = cnf.get("jwk");
|
||||||
if (cnfJwk == null) {
|
if (cnfJwk == null) {
|
||||||
throw new UnsupportedOperationException("Only cnf/jwk claim supported");
|
throw new UnsupportedOperationException("Only cnf/jwk claim supported");
|
||||||
}
|
}
|
||||||
|
@ -394,7 +396,7 @@ public class SdJwtVerificationContext {
|
||||||
Set<String> visitedSalts = new HashSet<>();
|
Set<String> visitedSalts = new HashSet<>();
|
||||||
Set<String> visitedDigests = new HashSet<>();
|
Set<String> visitedDigests = new HashSet<>();
|
||||||
Set<String> visitedDisclosureStrings = new HashSet<>();
|
Set<String> visitedDisclosureStrings = new HashSet<>();
|
||||||
var disclosedPayload = validateViaRecursiveDisclosing(
|
JsonNode disclosedPayload = validateViaRecursiveDisclosing(
|
||||||
SdJwtUtils.deepClone(issuerSignedJwt.getPayload()),
|
SdJwtUtils.deepClone(issuerSignedJwt.getPayload()),
|
||||||
visitedSalts, visitedDigests, visitedDisclosureStrings);
|
visitedSalts, visitedDigests, visitedDisclosureStrings);
|
||||||
|
|
||||||
|
@ -427,11 +429,11 @@ public class SdJwtVerificationContext {
|
||||||
|
|
||||||
// Find all objects having an _sd key that refers to an array of strings.
|
// Find all objects having an _sd key that refers to an array of strings.
|
||||||
if (currentNode.isObject()) {
|
if (currentNode.isObject()) {
|
||||||
var currentObjectNode = ((ObjectNode) currentNode);
|
ObjectNode currentObjectNode = ((ObjectNode) currentNode);
|
||||||
|
|
||||||
var sdArray = currentObjectNode.get(IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE);
|
JsonNode sdArray = currentObjectNode.get(IssuerSignedJWT.CLAIM_NAME_SELECTIVE_DISCLOSURE);
|
||||||
if (sdArray != null && sdArray.isArray()) {
|
if (sdArray != null && sdArray.isArray()) {
|
||||||
for (var el : sdArray) {
|
for (JsonNode el : sdArray) {
|
||||||
if (!el.isTextual()) {
|
if (!el.isTextual()) {
|
||||||
throw new VerificationException(
|
throw new VerificationException(
|
||||||
"Unexpected non-string element inside _sd array: " + el
|
"Unexpected non-string element inside _sd array: " + el
|
||||||
|
@ -441,16 +443,16 @@ public class SdJwtVerificationContext {
|
||||||
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
||||||
// If no such Disclosure can be found, the digest MUST be ignored.
|
// If no such Disclosure can be found, the digest MUST be ignored.
|
||||||
|
|
||||||
var digest = el.asText();
|
String digest = el.asText();
|
||||||
markDigestAsVisited(digest, visitedDigests);
|
markDigestAsVisited(digest, visitedDigests);
|
||||||
var disclosure = disclosures.get(digest);
|
String disclosure = disclosures.get(digest);
|
||||||
|
|
||||||
if (disclosure != null) {
|
if (disclosure != null) {
|
||||||
// Mark disclosure as visited
|
// Mark disclosure as visited
|
||||||
visitedDisclosureStrings.add(disclosure);
|
visitedDisclosureStrings.add(disclosure);
|
||||||
|
|
||||||
// Validate disclosure format
|
// Validate disclosure format
|
||||||
var decodedDisclosure = validateSdArrayDigestDisclosureFormat(disclosure);
|
DisclosureFields decodedDisclosure = validateSdArrayDigestDisclosureFormat(disclosure);
|
||||||
|
|
||||||
// Mark salt as visited
|
// Mark salt as visited
|
||||||
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
||||||
|
@ -475,29 +477,29 @@ public class SdJwtVerificationContext {
|
||||||
|
|
||||||
// Find all array elements that are objects with one key, that key being ... and referring to a string
|
// Find all array elements that are objects with one key, that key being ... and referring to a string
|
||||||
if (currentNode.isArray()) {
|
if (currentNode.isArray()) {
|
||||||
var currentArrayNode = ((ArrayNode) currentNode);
|
ArrayNode currentArrayNode = ((ArrayNode) currentNode);
|
||||||
var indexesToRemove = new ArrayList<Integer>();
|
ArrayList<Integer> indexesToRemove = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0; i < currentArrayNode.size(); ++i) {
|
for (int i = 0; i < currentArrayNode.size(); ++i) {
|
||||||
var itemNode = currentArrayNode.get(i);
|
JsonNode itemNode = currentArrayNode.get(i);
|
||||||
if (itemNode.isObject() && itemNode.size() == 1) {
|
if (itemNode.isObject() && itemNode.size() == 1) {
|
||||||
// Check single "..." field
|
// Check single "..." field
|
||||||
var field = itemNode.fields().next();
|
Map.Entry<String, JsonNode> field = itemNode.fields().next();
|
||||||
if (field.getKey().equals(UndisclosedArrayElement.SD_CLAIM_NAME)
|
if (field.getKey().equals(UndisclosedArrayElement.SD_CLAIM_NAME)
|
||||||
&& field.getValue().isTextual()) {
|
&& field.getValue().isTextual()) {
|
||||||
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
||||||
// If no such Disclosure can be found, the digest MUST be ignored.
|
// If no such Disclosure can be found, the digest MUST be ignored.
|
||||||
|
|
||||||
var digest = field.getValue().asText();
|
String digest = field.getValue().asText();
|
||||||
markDigestAsVisited(digest, visitedDigests);
|
markDigestAsVisited(digest, visitedDigests);
|
||||||
var disclosure = disclosures.get(digest);
|
String disclosure = disclosures.get(digest);
|
||||||
|
|
||||||
if (disclosure != null) {
|
if (disclosure != null) {
|
||||||
// Mark disclosure as visited
|
// Mark disclosure as visited
|
||||||
visitedDisclosureStrings.add(disclosure);
|
visitedDisclosureStrings.add(disclosure);
|
||||||
|
|
||||||
// Validate disclosure format
|
// Validate disclosure format
|
||||||
var decodedDisclosure = validateArrayElementDigestDisclosureFormat(disclosure);
|
DisclosureFields decodedDisclosure = validateArrayElementDigestDisclosureFormat(disclosure);
|
||||||
|
|
||||||
// Mark salt as visited
|
// Mark salt as visited
|
||||||
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
||||||
|
@ -584,10 +586,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.
|
||||||
|
|
||||||
var denylist = List.of(
|
List<String> denylist = Arrays.asList(new String[]{
|
||||||
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)) {
|
||||||
|
|
108
core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
Normal file
108
core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 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.jose.jwk;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.interfaces.EdECPublicKey;
|
||||||
|
import java.security.spec.EdECPoint;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeyUtils;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.crypto.KeyUse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class JWKBuilder extends AbstractJWKBuilder {
|
||||||
|
|
||||||
|
private JWKBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JWKBuilder create() {
|
||||||
|
return new JWKBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKBuilder kid(String kid) {
|
||||||
|
this.kid = kid;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKBuilder algorithm(String algorithm) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JWK okp(Key key) {
|
||||||
|
return okp(key, DEFAULT_PUBLIC_KEY_USE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JWK okp(Key key, KeyUse keyUse) {
|
||||||
|
EdECPublicKey eddsaPublicKey = (EdECPublicKey) key;
|
||||||
|
|
||||||
|
OKPPublicJWK k = new OKPPublicJWK();
|
||||||
|
|
||||||
|
String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
|
||||||
|
|
||||||
|
k.setKeyId(kid);
|
||||||
|
k.setKeyType(KeyType.OKP);
|
||||||
|
k.setAlgorithm(algorithm);
|
||||||
|
k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName());
|
||||||
|
k.setCrv(eddsaPublicKey.getParams().getName());
|
||||||
|
|
||||||
|
Optional<String> x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
|
||||||
|
k.setX(x.orElse(""));
|
||||||
|
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) {
|
||||||
|
EdECPoint edEcPoint = eddsaPublicKey.getPoint();
|
||||||
|
BigInteger yCoordinate = edEcPoint.getY();
|
||||||
|
|
||||||
|
// JWK representation "x" of a public key
|
||||||
|
int bytesLength = 0;
|
||||||
|
if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) {
|
||||||
|
bytesLength = 32;
|
||||||
|
} else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) {
|
||||||
|
bytesLength = 57;
|
||||||
|
} else {
|
||||||
|
return Optional.ofNullable(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate.
|
||||||
|
byte[] yCoordinateLittleEndianBytes = new byte[bytesLength];
|
||||||
|
|
||||||
|
// convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027)
|
||||||
|
yCoordinateLittleEndianBytes = Arrays.copyOf(reverseBytes(yCoordinate.toByteArray()), bytesLength);
|
||||||
|
|
||||||
|
// set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037)
|
||||||
|
if (edEcPoint.isXOdd()) {
|
||||||
|
yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
127
core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
Executable file
127
core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java
Executable file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 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.jose.jwk;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.EdECPoint;
|
||||||
|
import java.security.spec.EdECPublicKeySpec;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.NamedParameterSpec;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.crypto.Algorithm;
|
||||||
|
import org.keycloak.crypto.KeyType;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class JWKParser extends AbstractJWKParser {
|
||||||
|
|
||||||
|
private JWKParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JWKParser create() {
|
||||||
|
return new JWKParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKParser(JWK jwk) {
|
||||||
|
this.jwk = jwk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JWKParser create(JWK jwk) {
|
||||||
|
return new JWKParser(jwk);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWKParser parse(String jwk) {
|
||||||
|
try {
|
||||||
|
this.jwk = JsonSerialization.mapper.readValue(jwk, JWK.class);
|
||||||
|
return this;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKey toPublicKey() {
|
||||||
|
if (jwk == null) {
|
||||||
|
throw new IllegalStateException("Not possible to convert to the publicKey. The jwk is not set");
|
||||||
|
}
|
||||||
|
String keyType = jwk.getKeyType();
|
||||||
|
if (KeyType.RSA.equals(keyType)) {
|
||||||
|
return createRSAPublicKey();
|
||||||
|
} else if (KeyType.EC.equals(keyType)) {
|
||||||
|
return createECPublicKey();
|
||||||
|
} else if (KeyType.OKP.equals(keyType)) {
|
||||||
|
return createOKPPublicKey();
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unsupported keyType " + keyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKey createOKPPublicKey() {
|
||||||
|
String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X);
|
||||||
|
String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV);
|
||||||
|
// JWK representation "x" of a public key
|
||||||
|
int bytesLength = 0;
|
||||||
|
if (Algorithm.Ed25519.equals(crv)) {
|
||||||
|
bytesLength = 32;
|
||||||
|
} else if (Algorithm.Ed448.equals(crv)) {
|
||||||
|
bytesLength = 57;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Invalid JWK representation of OKP type algorithm");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decodedX = Base64Url.decode(x);
|
||||||
|
if (decodedX.length != bytesLength) {
|
||||||
|
throw new RuntimeException("Invalid JWK representation of OKP type public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even
|
||||||
|
boolean isOddX = false;
|
||||||
|
if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000
|
||||||
|
isOddX = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0
|
||||||
|
decodedX[decodedX.length - 1] &= 127; // 0b01111111
|
||||||
|
|
||||||
|
// both x and y-coordinate in twisted Edwards curve are always 0 or natural number
|
||||||
|
BigInteger y = new BigInteger(1, JWKBuilder.reverseBytes(decodedX));
|
||||||
|
NamedParameterSpec spec = new NamedParameterSpec(crv);
|
||||||
|
EdECPoint ep = new EdECPoint(isOddX, y);
|
||||||
|
EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep);
|
||||||
|
|
||||||
|
PublicKey publicKey = null;
|
||||||
|
try {
|
||||||
|
publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec);
|
||||||
|
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isKeyTypeSupported(String keyType) {
|
||||||
|
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) || OKPPublicJWK.OKP.equals(keyType));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import org.keycloak.util.TokenUtil;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ public abstract class RSAVerifierTest {
|
||||||
|
|
||||||
String encoded = new JWSBuilder()
|
String encoded = new JWSBuilder()
|
||||||
.jwk(jwk)
|
.jwk(jwk)
|
||||||
.x5c(List.of(idpCertificate, caCertificate))
|
.x5c(Arrays.asList(new X509Certificate[]{idpCertificate, caCertificate}))
|
||||||
.jsonContent(token)
|
.jsonContent(token)
|
||||||
.rsa256(idpPair.getPrivate());
|
.rsa256(idpPair.getPrivate());
|
||||||
TokenVerifier tokenVerifier = TokenVerifier.create(encoded, JsonWebToken.class);
|
TokenVerifier tokenVerifier = TokenVerifier.create(encoded, JsonWebToken.class);
|
||||||
|
|
|
@ -26,6 +26,9 @@ import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.rule.CryptoInitRule;
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -44,7 +47,7 @@ public abstract class SdJwsTest {
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
ObjectNode node = mapper.createObjectNode();
|
ObjectNode node = mapper.createObjectNode();
|
||||||
node.put("sub", "test");
|
node.put("sub", "test");
|
||||||
node.put("exp", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
node.put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
|
||||||
node.put("name", "Test User");
|
node.put("name", "Test User");
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +69,7 @@ public abstract class SdJwsTest {
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyExpClaim_ExpiredJWT() {
|
public void testVerifyExpClaim_ExpiredJWT() {
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("exp", Instant.now().minus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
((ObjectNode) payload).put("exp", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond());
|
||||||
SdJws sdJws = new SdJws(payload) {
|
SdJws sdJws = new SdJws(payload) {
|
||||||
};
|
};
|
||||||
assertThrows(VerificationException.class, sdJws::verifyExpClaim);
|
assertThrows(VerificationException.class, sdJws::verifyExpClaim);
|
||||||
|
@ -75,7 +78,7 @@ public abstract class SdJwsTest {
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyExpClaim_Positive() throws Exception {
|
public void testVerifyExpClaim_Positive() throws Exception {
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("exp", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
((ObjectNode) payload).put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
|
||||||
SdJws sdJws = new SdJws(payload) {
|
SdJws sdJws = new SdJws(payload) {
|
||||||
};
|
};
|
||||||
sdJws.verifyExpClaim();
|
sdJws.verifyExpClaim();
|
||||||
|
@ -84,7 +87,7 @@ public abstract class SdJwsTest {
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyNotBeforeClaim_Negative() {
|
public void testVerifyNotBeforeClaim_Negative() {
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("nbf", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
((ObjectNode) payload).put("nbf", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
|
||||||
SdJws sdJws = new SdJws(payload) {
|
SdJws sdJws = new SdJws(payload) {
|
||||||
};
|
};
|
||||||
assertThrows(VerificationException.class, sdJws::verifyNotBeforeClaim);
|
assertThrows(VerificationException.class, sdJws::verifyNotBeforeClaim);
|
||||||
|
@ -93,7 +96,7 @@ public abstract class SdJwsTest {
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyNotBeforeClaim_Positive() throws Exception {
|
public void testVerifyNotBeforeClaim_Positive() throws Exception {
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("nbf", Instant.now().minus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
((ObjectNode) payload).put("nbf", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond());
|
||||||
SdJws sdJws = new SdJws(payload) {
|
SdJws sdJws = new SdJws(payload) {
|
||||||
};
|
};
|
||||||
sdJws.verifyNotBeforeClaim();
|
sdJws.verifyNotBeforeClaim();
|
||||||
|
@ -124,17 +127,17 @@ public abstract class SdJwsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyIssClaim_Negative() {
|
public void testVerifyIssClaim_Negative() {
|
||||||
List<String> allowedIssuers = List.of("issuer1@sdjwt.com", "issuer2@sdjwt.com");
|
List<String> allowedIssuers = Arrays.asList(new String[]{"issuer1@sdjwt.com", "issuer2@sdjwt.com"});
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("iss", "unknown-issuer@sdjwt.com");
|
((ObjectNode) payload).put("iss", "unknown-issuer@sdjwt.com");
|
||||||
SdJws sdJws = new SdJws(payload) {};
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyIssClaim(allowedIssuers));
|
VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyIssClaim(allowedIssuers));
|
||||||
assertEquals("Unknown 'iss' claim value: unknown-issuer@sdjwt.com", exception.getMessage());
|
assertEquals("Unknown 'iss' claim value: unknown-issuer@sdjwt.com", exception.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testVerifyIssClaim_Positive() throws VerificationException {
|
public void testVerifyIssClaim_Positive() throws VerificationException {
|
||||||
List<String> allowedIssuers = List.of("issuer1@sdjwt.com", "issuer2@sdjwt.com");
|
List<String> allowedIssuers = Arrays.asList(new String[]{"issuer1@sdjwt.com", "issuer2@sdjwt.com"});
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("iss", "issuer1@sdjwt.com");
|
((ObjectNode) payload).put("iss", "issuer1@sdjwt.com");
|
||||||
SdJws sdJws = new SdJws(payload) {};
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
|
@ -146,7 +149,7 @@ public abstract class SdJwsTest {
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("vct", "IdentityCredential");
|
((ObjectNode) payload).put("vct", "IdentityCredential");
|
||||||
SdJws sdJws = new SdJws(payload) {};
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyVctClaim(List.of("PassportCredential")));
|
VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyVctClaim(Collections.singletonList("PassportCredential")));
|
||||||
assertEquals("Unknown 'vct' claim value: IdentityCredential", exception.getMessage());
|
assertEquals("Unknown 'vct' claim value: IdentityCredential", exception.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,26 +158,26 @@ public abstract class SdJwsTest {
|
||||||
JsonNode payload = createPayload();
|
JsonNode payload = createPayload();
|
||||||
((ObjectNode) payload).put("vct", "IdentityCredential");
|
((ObjectNode) payload).put("vct", "IdentityCredential");
|
||||||
SdJws sdJws = new SdJws(payload) {};
|
SdJws sdJws = new SdJws(payload) {};
|
||||||
sdJws.verifyVctClaim(List.of("IdentityCredential"));
|
sdJws.verifyVctClaim(Collections.singletonList("IdentityCredential"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldValidateAgeSinceIssued() throws VerificationException {
|
public void shouldValidateAgeSinceIssued() throws VerificationException {
|
||||||
long now = Instant.now().getEpochSecond();
|
long now = Instant.now().getEpochSecond();
|
||||||
var sdJws = exampleSdJws(now);
|
SdJws sdJws = exampleSdJws(now);
|
||||||
sdJws.verifyAge(180);
|
sdJws.verifyAge(180);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() {
|
public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() {
|
||||||
long now = Instant.now().getEpochSecond();
|
long now = Instant.now().getEpochSecond();
|
||||||
var sdJws = exampleSdJws(now - 1000); // that will be too old
|
SdJws sdJws = exampleSdJws(now - 1000); // that will be too old
|
||||||
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180));
|
VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180));
|
||||||
assertEquals("jwt is too old", exception.getMessage());
|
assertEquals("jwt is too old", exception.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SdJws exampleSdJws(long iat) {
|
private SdJws exampleSdJws(long iat) {
|
||||||
var payload = SdJwtUtils.mapper.createObjectNode();
|
ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
|
||||||
payload.set("iat", SdJwtUtils.mapper.valueToTree(iat));
|
payload.set("iat", SdJwtUtils.mapper.valueToTree(iat));
|
||||||
|
|
||||||
return new SdJws(payload) {
|
return new SdJws(payload) {
|
||||||
|
|
|
@ -26,8 +26,9 @@ import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.rule.CryptoInitRule;
|
import org.keycloak.rule.CryptoInitRule;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.Arrays;
|
||||||
import java.util.Set;
|
import java.util.Collections;
|
||||||
|
import org.keycloak.crypto.SignatureSignerContext;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.endsWith;
|
import static org.hamcrest.CoreMatchers.endsWith;
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
@ -50,14 +51,14 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void settingsTest() {
|
public void settingsTest() {
|
||||||
var issuerSignerContext = testSettings.issuerSigContext;
|
SignatureSignerContext issuerSignerContext = testSettings.issuerSigContext;
|
||||||
assertNotNull(issuerSignerContext);
|
assertNotNull(issuerSignerContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
|
public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
|
||||||
for (String hashAlg : List.of("sha-256", "sha-384", "sha-512")) {
|
for (String hashAlg : Arrays.asList(new String[]{"sha-256", "sha-384", "sha-512"})) {
|
||||||
var sdJwt = exampleFlatSdJwtV1()
|
SdJwt sdJwt = exampleFlatSdJwtV1()
|
||||||
.withHashAlgorithm(hashAlg)
|
.withHashAlgorithm(hashAlg)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -67,36 +68,36 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
|
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
|
||||||
var sdJwt = exampleFlatSdJwtV1().build();
|
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException {
|
public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException {
|
||||||
var sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
SdJwt sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception {
|
public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception {
|
||||||
var sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
SdJwt sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSdJwtVerification_RecursiveSdJwt() throws Exception {
|
public void testSdJwtVerification_RecursiveSdJwt() throws Exception {
|
||||||
var sdJwt = exampleRecursiveSdJwtV1().build();
|
SdJwt sdJwt = exampleRecursiveSdJwtV1().build();
|
||||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sdJwtVerificationShouldFail_OnInsecureHashAlg() {
|
public void sdJwtVerificationShouldFail_OnInsecureHashAlg() {
|
||||||
var sdJwt = exampleFlatSdJwtV1()
|
SdJwt sdJwt = exampleFlatSdJwtV1()
|
||||||
.withHashAlgorithm("sha-224") // not deemed secure
|
.withHashAlgorithm("sha-224") // not deemed secure
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
);
|
);
|
||||||
|
@ -106,8 +107,8 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sdJwtVerificationShouldFail_WithWrongVerifier() {
|
public void sdJwtVerificationShouldFail_WithWrongVerifier() {
|
||||||
var sdJwt = exampleFlatSdJwtV1().build();
|
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withVerifier(testSettings.holderVerifierContext) // wrong verifier
|
.withVerifier(testSettings.holderVerifierContext) // wrong verifier
|
||||||
|
@ -127,15 +128,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
claimSet.put("exp", now - 1000); // expired 1000 seconds ago
|
claimSet.put("exp", now - 1000); // expired 1000 seconds ago
|
||||||
|
|
||||||
// Exp claim is plain
|
// Exp claim is plain
|
||||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
// Exp claim is undisclosed
|
// Exp claim is undisclosed
|
||||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
.withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet()))
|
||||||
.withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateExpirationClaim(true)
|
.withValidateExpirationClaim(true)
|
||||||
|
@ -162,11 +163,11 @@ public abstract class SdJwtVerificationTest {
|
||||||
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
||||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateExpirationClaim(true)
|
.withValidateExpirationClaim(true)
|
||||||
|
@ -187,15 +188,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
claimSet.put("iat", now + 1000); // issued in the future
|
claimSet.put("iat", now + 1000); // issued in the future
|
||||||
|
|
||||||
// Exp claim is plain
|
// Exp claim is plain
|
||||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
// Exp claim is undisclosed
|
// Exp claim is undisclosed
|
||||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
.withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet()))
|
||||||
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateIssuedAtClaim(true)
|
.withValidateIssuedAtClaim(true)
|
||||||
|
@ -216,15 +217,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
claimSet.put("nbf", now + 1000); // now will be too soon to accept the jwt
|
claimSet.put("nbf", now + 1000); // now will be too soon to accept the jwt
|
||||||
|
|
||||||
// Exp claim is plain
|
// Exp claim is plain
|
||||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
// Exp claim is undisclosed
|
// Exp claim is undisclosed
|
||||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
.withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet()))
|
||||||
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
.withValidateNotBeforeClaim(true)
|
.withValidateNotBeforeClaim(true)
|
||||||
|
@ -242,9 +243,9 @@ public abstract class SdJwtVerificationTest {
|
||||||
claimSet.put("given_name", "John");
|
claimSet.put("given_name", "John");
|
||||||
claimSet.set("_sd", mapper.readTree("[123]"));
|
claimSet.set("_sd", mapper.readTree("[123]"));
|
||||||
|
|
||||||
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||||
.build())
|
.build())
|
||||||
|
@ -255,15 +256,15 @@ public abstract class SdJwtVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() {
|
public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() {
|
||||||
for (String forbiddenClaimName : List.of("_sd", "...")) {
|
for (String forbiddenClaimName : Arrays.asList(new String[]{"_sd", "..."})) {
|
||||||
ObjectNode claimSet = mapper.createObjectNode();
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
claimSet.put(forbiddenClaimName, "Value");
|
claimSet.put(forbiddenClaimName, "Value");
|
||||||
|
|
||||||
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
.withUndisclosedClaim(forbiddenClaimName, "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim(forbiddenClaimName, "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
);
|
);
|
||||||
|
@ -277,13 +278,13 @@ public abstract class SdJwtVerificationTest {
|
||||||
ObjectNode claimSet = mapper.createObjectNode();
|
ObjectNode claimSet = mapper.createObjectNode();
|
||||||
claimSet.put("given_name", "John"); // this same field will also be nested
|
claimSet.put("given_name", "John"); // this same field will also be nested
|
||||||
|
|
||||||
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||||
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||||
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
);
|
);
|
||||||
|
@ -297,14 +298,14 @@ public abstract class SdJwtVerificationTest {
|
||||||
claimSet.put("given_name", "John");
|
claimSet.put("given_name", "John");
|
||||||
claimSet.put("family_name", "Doe");
|
claimSet.put("family_name", "Doe");
|
||||||
|
|
||||||
var salt = "eluV5Og3gSNII8EYnsxA_A";
|
String salt = "eluV5Og3gSNII8EYnsxA_A";
|
||||||
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||||
.withUndisclosedClaim("given_name", salt)
|
.withUndisclosedClaim("given_name", salt)
|
||||||
// We are reusing the same salt value, and that is the problem
|
// We are reusing the same salt value, and that is the problem
|
||||||
.withUndisclosedClaim("family_name", salt)
|
.withUndisclosedClaim("family_name", salt)
|
||||||
.build()).build();
|
.build()).build();
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
||||||
import org.keycloak.sdjwt.vp.SdJwtVP;
|
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
@ -69,9 +70,11 @@ 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 {
|
||||||
var entries = List.of("sdjwt/s20.8-sdjwt+kb--es384.txt", "sdjwt/s20.8-sdjwt+kb--es512.txt");
|
List<String> entries = Arrays.asList(new String[]{
|
||||||
|
"sdjwt/s20.8-sdjwt+kb--es384.txt", "sdjwt/s20.8-sdjwt+kb--es512.txt"
|
||||||
|
});
|
||||||
|
|
||||||
for (var 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);
|
||||||
|
|
||||||
|
@ -84,14 +87,14 @@ 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 {
|
||||||
var entries = List.of(
|
List<String> entries = Arrays.asList(new String[]{
|
||||||
"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 (var 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);
|
||||||
|
|
||||||
|
@ -220,7 +223,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldFail_IfKbSdHashWrongFormat() {
|
public void testShouldFail_IfKbSdHashWrongFormat() {
|
||||||
var kbPayload = exampleKbPayload();
|
ObjectNode kbPayload = exampleKbPayload();
|
||||||
|
|
||||||
// This hash is not a string
|
// This hash is not a string
|
||||||
kbPayload.set("sd_hash", mapper.valueToTree(1234));
|
kbPayload.set("sd_hash", mapper.valueToTree(1234));
|
||||||
|
@ -235,7 +238,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testShouldFail_IfKbSdHashInvalid() {
|
public void testShouldFail_IfKbSdHashInvalid() {
|
||||||
var kbPayload = exampleKbPayload();
|
ObjectNode kbPayload = exampleKbPayload();
|
||||||
|
|
||||||
// This hash makes no sense
|
// This hash makes no sense
|
||||||
kbPayload.put("sd_hash", "c3FmZHFmZGZlZXNkZmZi");
|
kbPayload.put("sd_hash", "c3FmZHFmZGZlZXNkZmZi");
|
||||||
|
@ -252,7 +255,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
public void testShouldFail_IfKbIssuedInFuture() {
|
public void testShouldFail_IfKbIssuedInFuture() {
|
||||||
long now = Instant.now().getEpochSecond();
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
var kbPayload = exampleKbPayload();
|
ObjectNode kbPayload = exampleKbPayload();
|
||||||
kbPayload.set("iat", mapper.valueToTree(now + 1000));
|
kbPayload.set("iat", mapper.valueToTree(now + 1000));
|
||||||
|
|
||||||
testShouldFailGeneric2(
|
testShouldFailGeneric2(
|
||||||
|
@ -267,7 +270,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
public void testShouldFail_IfKbTooOld() {
|
public void testShouldFail_IfKbTooOld() {
|
||||||
long issuerSignedJwtIat = 1683000000; // same value in test vector
|
long issuerSignedJwtIat = 1683000000; // same value in test vector
|
||||||
|
|
||||||
var kbPayload = exampleKbPayload();
|
ObjectNode kbPayload = exampleKbPayload();
|
||||||
// This KB-JWT is then issued more than 60s ago
|
// This KB-JWT is then issued more than 60s ago
|
||||||
kbPayload.set("iat", mapper.valueToTree(issuerSignedJwtIat - 120));
|
kbPayload.set("iat", mapper.valueToTree(issuerSignedJwtIat - 120));
|
||||||
|
|
||||||
|
@ -285,7 +288,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
public void testShouldFail_IfKbExpired() {
|
public void testShouldFail_IfKbExpired() {
|
||||||
long now = Instant.now().getEpochSecond();
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
var kbPayload = exampleKbPayload();
|
ObjectNode kbPayload = exampleKbPayload();
|
||||||
kbPayload.set("exp", mapper.valueToTree(now - 1000));
|
kbPayload.set("exp", mapper.valueToTree(now - 1000));
|
||||||
|
|
||||||
testShouldFailGeneric2(
|
testShouldFailGeneric2(
|
||||||
|
@ -302,7 +305,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
public void testShouldFail_IfKbNotBeforeTimeYet() {
|
public void testShouldFail_IfKbNotBeforeTimeYet() {
|
||||||
long now = Instant.now().getEpochSecond();
|
long now = Instant.now().getEpochSecond();
|
||||||
|
|
||||||
var kbPayload = exampleKbPayload();
|
ObjectNode kbPayload = exampleKbPayload();
|
||||||
kbPayload.set("nbf", mapper.valueToTree(now + 1000));
|
kbPayload.set("nbf", mapper.valueToTree(now + 1000));
|
||||||
|
|
||||||
testShouldFailGeneric2(
|
testShouldFailGeneric2(
|
||||||
|
@ -321,7 +324,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt");
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt");
|
||||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
var exception = assertThrows(
|
UnsupportedOperationException exception = assertThrows(
|
||||||
UnsupportedOperationException.class,
|
UnsupportedOperationException.class,
|
||||||
() -> sdJwtVP.verify(
|
() -> sdJwtVP.verify(
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
@ -363,7 +366,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), testFilePath);
|
String sdJwtVPString = TestUtils.readFileAsString(getClass(), testFilePath);
|
||||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwtVP.verify(
|
() -> sdJwtVP.verify(
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
@ -399,7 +402,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
+ keyBindingJWT.toJws()
|
+ keyBindingJWT.toJws()
|
||||||
);
|
);
|
||||||
|
|
||||||
var exception = assertThrows(
|
VerificationException exception = assertThrows(
|
||||||
VerificationException.class,
|
VerificationException.class,
|
||||||
() -> sdJwtVP.verify(
|
() -> sdJwtVP.verify(
|
||||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||||
|
@ -431,7 +434,7 @@ public abstract class SdJwtVPVerificationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObjectNode exampleKbPayload() {
|
private ObjectNode exampleKbPayload() {
|
||||||
var payload = mapper.createObjectNode();
|
ObjectNode payload = mapper.createObjectNode();
|
||||||
payload.put("nonce", "1234567890");
|
payload.put("nonce", "1234567890");
|
||||||
payload.put("aud", "https://verifier.example.org");
|
payload.put("aud", "https://verifier.example.org");
|
||||||
payload.put("sd_hash", "X9RrrfWt_70gHzOcovGSIt4Fms9Tf2g2hjlWVI_cxZg");
|
payload.put("sd_hash", "X9RrrfWt_70gHzOcovGSIt4Fms9Tf2g2hjlWVI_cxZg");
|
||||||
|
|
|
@ -32,6 +32,12 @@
|
||||||
For the use by 3rd party applications, please use `org.keycloak:keycloak-admin-client` module.
|
For the use by 3rd party applications, please use `org.keycloak:keycloak-admin-client` module.
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<maven.compiler.release>8</maven.compiler.release>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
Loading…
Reference in a new issue