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=","
|
||||
done
|
||||
|
||||
./mvnw test -pl "$PROJECTS" -am
|
||||
./mvnw install -pl "$PROJECTS" -am
|
||||
|
||||
- name: Upload JVM Heapdumps
|
||||
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.
|
||||
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>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
|
|
|
@ -32,8 +32,9 @@
|
|||
<description>Common library and dependencies shared with server and all adapters</description>
|
||||
|
||||
<properties>
|
||||
<!-- We still need to support EAP 8, set the Java version to 11. -->
|
||||
<maven.compiler.release>11</maven.compiler.release>
|
||||
<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>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||
</properties>
|
||||
|
|
|
@ -988,7 +988,7 @@ public class Reflections {
|
|||
* @throws InstantiationException
|
||||
* @deprecated for removal in Keycloak 27
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
@Deprecated
|
||||
public static <T> T newInstance(final Class<T> fromClass) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
|
||||
return newInstance(fromClass, fromClass.getName());
|
||||
}
|
||||
|
@ -1008,7 +1008,7 @@ public class Reflections {
|
|||
* @throws InstantiationException
|
||||
* @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 {
|
||||
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)}
|
||||
* @deprecated for removal in Keycloak 27
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
@Deprecated
|
||||
public class SetAccessiblePrivilegedAction implements PrivilegedAction<Void> {
|
||||
|
||||
private final AccessibleObject member;
|
||||
|
|
|
@ -24,7 +24,7 @@ import java.security.PrivilegedAction;
|
|||
* A {@link PrivilegedAction} that calls {@link AccessibleObject#setAccessible(boolean)}
|
||||
* @deprecated for removal in Keycloak 27
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
@Deprecated
|
||||
public class UnSetAccessiblePrivilegedAction implements PrivilegedAction<Void> {
|
||||
|
||||
private final AccessibleObject member;
|
||||
|
|
35
core/pom.xml
35
core/pom.xml
|
@ -32,6 +32,9 @@
|
|||
<description/>
|
||||
|
||||
<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>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||
</properties>
|
||||
|
@ -72,6 +75,38 @@
|
|||
</dependency>
|
||||
</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>
|
||||
<resources>
|
||||
<resource>
|
||||
|
|
|
@ -17,19 +17,10 @@
|
|||
|
||||
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 java.security.Key;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
@ -54,55 +45,13 @@ public class JWKBuilder extends AbstractJWKBuilder {
|
|||
|
||||
@Override
|
||||
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
|
||||
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;
|
||||
// not supported if jdk version < 17
|
||||
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -140,7 +142,7 @@ public class IssuerSignedJWT extends SdJws {
|
|||
* Returns `cnf` claim (establishing key binding)
|
||||
*/
|
||||
public Optional<JsonNode> getCnfClaim() {
|
||||
var cnf = getPayload().get("cnf");
|
||||
JsonNode cnf = getPayload().get("cnf");
|
||||
return Optional.ofNullable(cnf);
|
||||
}
|
||||
|
||||
|
@ -148,7 +150,7 @@ public class IssuerSignedJWT extends SdJws {
|
|||
* Returns declared hash algorithm from SD hash claim.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -159,10 +161,10 @@ public class IssuerSignedJWT extends SdJws {
|
|||
*/
|
||||
public void verifySdHashAlgorithm() throws VerificationException {
|
||||
// Known secure algorithms
|
||||
final Set<String> secureAlgorithms = Set.of(
|
||||
final Set<String> secureAlgorithms = new HashSet<>(Arrays.asList(
|
||||
"sha-256", "sha-384", "sha-512",
|
||||
"sha3-256", "sha3-384", "sha3-512"
|
||||
);
|
||||
));
|
||||
|
||||
// Read SD hash claim
|
||||
String hashAlg = getSdHashAlg();
|
||||
|
|
|
@ -83,7 +83,7 @@ public class SdJwtUtils {
|
|||
JsonNode jsonNode;
|
||||
|
||||
// Decode Base64URL-encoded disclosure
|
||||
var decoded = new String(decodeNoPad(disclosure));
|
||||
String decoded = new String(decodeNoPad(disclosure));
|
||||
|
||||
// Parse the disclosure string into a JSON array
|
||||
try {
|
||||
|
|
|
@ -32,7 +32,9 @@ import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
|||
import org.keycloak.util.JWKSUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -75,9 +77,9 @@ public class SdJwtVerificationContext {
|
|||
private Map<String, String> computeDigestDisclosureMap(List<String> disclosureStrings) {
|
||||
return disclosureStrings.stream()
|
||||
.map(disclosureString -> {
|
||||
var digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
||||
String digest = SdJwtUtils.hashAndBase64EncodeNoPad(
|
||||
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));
|
||||
}
|
||||
|
@ -103,7 +105,7 @@ public class SdJwtVerificationContext {
|
|||
validateIssuerSignedJwt(issuerSignedJwtVerificationOpts.getVerifier());
|
||||
|
||||
// Validate disclosures.
|
||||
var disclosedPayload = validateDisclosuresDigests();
|
||||
JsonNode disclosedPayload = validateDisclosuresDigests();
|
||||
|
||||
// Validate time claims.
|
||||
// Issuers will typically include claims controlling the validity of the SD-JWT in plaintext in the
|
||||
|
@ -182,13 +184,13 @@ public class SdJwtVerificationContext {
|
|||
validateKeyBindingJwtTyp();
|
||||
|
||||
// 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")
|
||||
);
|
||||
|
||||
// Ensure that a signing algorithm was used that was deemed secure for the application.
|
||||
// The none algorithm MUST NOT be accepted.
|
||||
var holderVerifier = buildHolderVerifier(cnf);
|
||||
SignatureVerifierContext holderVerifier = buildHolderVerifier(cnf);
|
||||
|
||||
// Validate the signature over the Key Binding JWT
|
||||
try {
|
||||
|
@ -219,7 +221,7 @@ public class SdJwtVerificationContext {
|
|||
* @throws VerificationException if verification failed
|
||||
*/
|
||||
private void validateKeyBindingJwtTyp() throws VerificationException {
|
||||
var typ = keyBindingJwt.getHeader().getType();
|
||||
String typ = keyBindingJwt.getHeader().getType();
|
||||
if (!typ.equals(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);
|
||||
|
||||
// Read JWK
|
||||
var cnfJwk = cnf.get("jwk");
|
||||
JsonNode cnfJwk = cnf.get("jwk");
|
||||
if (cnfJwk == null) {
|
||||
throw new UnsupportedOperationException("Only cnf/jwk claim supported");
|
||||
}
|
||||
|
@ -394,7 +396,7 @@ public class SdJwtVerificationContext {
|
|||
Set<String> visitedSalts = new HashSet<>();
|
||||
Set<String> visitedDigests = new HashSet<>();
|
||||
Set<String> visitedDisclosureStrings = new HashSet<>();
|
||||
var disclosedPayload = validateViaRecursiveDisclosing(
|
||||
JsonNode disclosedPayload = validateViaRecursiveDisclosing(
|
||||
SdJwtUtils.deepClone(issuerSignedJwt.getPayload()),
|
||||
visitedSalts, visitedDigests, visitedDisclosureStrings);
|
||||
|
||||
|
@ -427,11 +429,11 @@ public class SdJwtVerificationContext {
|
|||
|
||||
// Find all objects having an _sd key that refers to an array of strings.
|
||||
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()) {
|
||||
for (var el : sdArray) {
|
||||
for (JsonNode el : sdArray) {
|
||||
if (!el.isTextual()) {
|
||||
throw new VerificationException(
|
||||
"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.
|
||||
// If no such Disclosure can be found, the digest MUST be ignored.
|
||||
|
||||
var digest = el.asText();
|
||||
String digest = el.asText();
|
||||
markDigestAsVisited(digest, visitedDigests);
|
||||
var disclosure = disclosures.get(digest);
|
||||
String disclosure = disclosures.get(digest);
|
||||
|
||||
if (disclosure != null) {
|
||||
// Mark disclosure as visited
|
||||
visitedDisclosureStrings.add(disclosure);
|
||||
|
||||
// Validate disclosure format
|
||||
var decodedDisclosure = validateSdArrayDigestDisclosureFormat(disclosure);
|
||||
DisclosureFields decodedDisclosure = validateSdArrayDigestDisclosureFormat(disclosure);
|
||||
|
||||
// Mark salt as visited
|
||||
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
|
||||
if (currentNode.isArray()) {
|
||||
var currentArrayNode = ((ArrayNode) currentNode);
|
||||
var indexesToRemove = new ArrayList<Integer>();
|
||||
ArrayNode currentArrayNode = ((ArrayNode) currentNode);
|
||||
ArrayList<Integer> indexesToRemove = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < currentArrayNode.size(); ++i) {
|
||||
var itemNode = currentArrayNode.get(i);
|
||||
JsonNode itemNode = currentArrayNode.get(i);
|
||||
if (itemNode.isObject() && itemNode.size() == 1) {
|
||||
// Check single "..." field
|
||||
var field = itemNode.fields().next();
|
||||
Map.Entry<String, JsonNode> field = itemNode.fields().next();
|
||||
if (field.getKey().equals(UndisclosedArrayElement.SD_CLAIM_NAME)
|
||||
&& field.getValue().isTextual()) {
|
||||
// Compare the value with the digests calculated previously and find the matching Disclosure.
|
||||
// If no such Disclosure can be found, the digest MUST be ignored.
|
||||
|
||||
var digest = field.getValue().asText();
|
||||
String digest = field.getValue().asText();
|
||||
markDigestAsVisited(digest, visitedDigests);
|
||||
var disclosure = disclosures.get(digest);
|
||||
String disclosure = disclosures.get(digest);
|
||||
|
||||
if (disclosure != null) {
|
||||
// Mark disclosure as visited
|
||||
visitedDisclosureStrings.add(disclosure);
|
||||
|
||||
// Validate disclosure format
|
||||
var decodedDisclosure = validateArrayElementDigestDisclosureFormat(disclosure);
|
||||
DisclosureFields decodedDisclosure = validateArrayElementDigestDisclosureFormat(disclosure);
|
||||
|
||||
// Mark salt as visited
|
||||
markSaltAsVisited(decodedDisclosure.getSaltValue(), visitedSalts);
|
||||
|
@ -584,10 +586,10 @@ public class SdJwtVerificationContext {
|
|||
|
||||
// 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,
|
||||
UndisclosedArrayElement.SD_CLAIM_NAME
|
||||
);
|
||||
});
|
||||
|
||||
String claimName = arrayNode.get(1).asText();
|
||||
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.KeyPairGenerator;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -96,7 +97,7 @@ public abstract class RSAVerifierTest {
|
|||
|
||||
String encoded = new JWSBuilder()
|
||||
.jwk(jwk)
|
||||
.x5c(List.of(idpCertificate, caCertificate))
|
||||
.x5c(Arrays.asList(new X509Certificate[]{idpCertificate, caCertificate}))
|
||||
.jsonContent(token)
|
||||
.rsa256(idpPair.getPrivate());
|
||||
TokenVerifier tokenVerifier = TokenVerifier.create(encoded, JsonWebToken.class);
|
||||
|
|
|
@ -26,6 +26,9 @@ import org.keycloak.common.VerificationException;
|
|||
import org.keycloak.rule.CryptoInitRule;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -44,7 +47,7 @@ public abstract class SdJwsTest {
|
|||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
node.put("sub", "test");
|
||||
node.put("exp", Instant.now().plus(1, TimeUnit.HOURS.toChronoUnit()).getEpochSecond());
|
||||
node.put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
|
||||
node.put("name", "Test User");
|
||||
return node;
|
||||
}
|
||||
|
@ -66,7 +69,7 @@ public abstract class SdJwsTest {
|
|||
@Test
|
||||
public void testVerifyExpClaim_ExpiredJWT() {
|
||||
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) {
|
||||
};
|
||||
assertThrows(VerificationException.class, sdJws::verifyExpClaim);
|
||||
|
@ -75,7 +78,7 @@ public abstract class SdJwsTest {
|
|||
@Test
|
||||
public void testVerifyExpClaim_Positive() throws Exception {
|
||||
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.verifyExpClaim();
|
||||
|
@ -84,7 +87,7 @@ public abstract class SdJwsTest {
|
|||
@Test
|
||||
public void testVerifyNotBeforeClaim_Negative() {
|
||||
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) {
|
||||
};
|
||||
assertThrows(VerificationException.class, sdJws::verifyNotBeforeClaim);
|
||||
|
@ -93,7 +96,7 @@ public abstract class SdJwsTest {
|
|||
@Test
|
||||
public void testVerifyNotBeforeClaim_Positive() throws Exception {
|
||||
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.verifyNotBeforeClaim();
|
||||
|
@ -124,17 +127,17 @@ public abstract class SdJwsTest {
|
|||
|
||||
@Test
|
||||
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();
|
||||
((ObjectNode) payload).put("iss", "unknown-issuer@sdjwt.com");
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
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();
|
||||
((ObjectNode) payload).put("iss", "issuer1@sdjwt.com");
|
||||
SdJws sdJws = new SdJws(payload) {};
|
||||
|
@ -146,7 +149,7 @@ public abstract class SdJwsTest {
|
|||
JsonNode payload = createPayload();
|
||||
((ObjectNode) payload).put("vct", "IdentityCredential");
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -155,26 +158,26 @@ public abstract class SdJwsTest {
|
|||
JsonNode payload = createPayload();
|
||||
((ObjectNode) payload).put("vct", "IdentityCredential");
|
||||
SdJws sdJws = new SdJws(payload) {};
|
||||
sdJws.verifyVctClaim(List.of("IdentityCredential"));
|
||||
sdJws.verifyVctClaim(Collections.singletonList("IdentityCredential"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldValidateAgeSinceIssued() throws VerificationException {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
var sdJws = exampleSdJws(now);
|
||||
SdJws sdJws = exampleSdJws(now);
|
||||
sdJws.verifyAge(180);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
var sdJws = exampleSdJws(now - 1000); // that will be too old
|
||||
var exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180));
|
||||
SdJws sdJws = exampleSdJws(now - 1000); // that will be too old
|
||||
VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180));
|
||||
assertEquals("jwt is too old", exception.getMessage());
|
||||
}
|
||||
|
||||
private SdJws exampleSdJws(long iat) {
|
||||
var payload = SdJwtUtils.mapper.createObjectNode();
|
||||
ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
|
||||
payload.set("iat", SdJwtUtils.mapper.valueToTree(iat));
|
||||
|
||||
return new SdJws(payload) {
|
||||
|
|
|
@ -26,8 +26,9 @@ import org.keycloak.common.VerificationException;
|
|||
import org.keycloak.rule.CryptoInitRule;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import org.keycloak.crypto.SignatureSignerContext;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.endsWith;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
|
@ -50,14 +51,14 @@ public abstract class SdJwtVerificationTest {
|
|||
|
||||
@Test
|
||||
public void settingsTest() {
|
||||
var issuerSignerContext = testSettings.issuerSigContext;
|
||||
SignatureSignerContext issuerSignerContext = testSettings.issuerSigContext;
|
||||
assertNotNull(issuerSignerContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSdJwtVerification_FlatSdJwt() throws VerificationException {
|
||||
for (String hashAlg : List.of("sha-256", "sha-384", "sha-512")) {
|
||||
var sdJwt = exampleFlatSdJwtV1()
|
||||
for (String hashAlg : Arrays.asList(new String[]{"sha-256", "sha-384", "sha-512"})) {
|
||||
SdJwt sdJwt = exampleFlatSdJwtV1()
|
||||
.withHashAlgorithm(hashAlg)
|
||||
.build();
|
||||
|
||||
|
@ -67,36 +68,36 @@ public abstract class SdJwtVerificationTest {
|
|||
|
||||
@Test
|
||||
public void testSdJwtVerification_EnforceIdempotence() throws VerificationException {
|
||||
var sdJwt = exampleFlatSdJwtV1().build();
|
||||
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSdJwtVerification_SdJwtWithUndisclosedNestedFields() throws VerificationException {
|
||||
var sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
||||
SdJwt sdJwt = exampleSdJwtWithUndisclosedNestedFieldsV1().build();
|
||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSdJwtVerification_SdJwtWithUndisclosedArrayElements() throws Exception {
|
||||
var sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
||||
SdJwt sdJwt = exampleSdJwtWithUndisclosedArrayElementsV1().build();
|
||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSdJwtVerification_RecursiveSdJwt() throws Exception {
|
||||
var sdJwt = exampleRecursiveSdJwtV1().build();
|
||||
SdJwt sdJwt = exampleRecursiveSdJwtV1().build();
|
||||
sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sdJwtVerificationShouldFail_OnInsecureHashAlg() {
|
||||
var sdJwt = exampleFlatSdJwtV1()
|
||||
SdJwt sdJwt = exampleFlatSdJwtV1()
|
||||
.withHashAlgorithm("sha-224") // not deemed secure
|
||||
.build();
|
||||
|
||||
var exception = assertThrows(
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||
);
|
||||
|
@ -106,8 +107,8 @@ public abstract class SdJwtVerificationTest {
|
|||
|
||||
@Test
|
||||
public void sdJwtVerificationShouldFail_WithWrongVerifier() {
|
||||
var sdJwt = exampleFlatSdJwtV1().build();
|
||||
var exception = assertThrows(
|
||||
SdJwt sdJwt = exampleFlatSdJwtV1().build();
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||
.withVerifier(testSettings.holderVerifierContext) // wrong verifier
|
||||
|
@ -127,15 +128,15 @@ public abstract class SdJwtVerificationTest {
|
|||
claimSet.put("exp", now - 1000); // expired 1000 seconds ago
|
||||
|
||||
// Exp claim is plain
|
||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||
// Exp claim is undisclosed
|
||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
||||
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet()))
|
||||
.withUndisclosedClaim("exp", "eluV5Og3gSNII8EYnsxA_A")
|
||||
.build()).build();
|
||||
|
||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||
var exception = assertThrows(
|
||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||
.withValidateExpirationClaim(true)
|
||||
|
@ -162,11 +163,11 @@ public abstract class SdJwtVerificationTest {
|
|||
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
|
||||
.build();
|
||||
|
||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
||||
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet1, disclosureSpec).build();
|
||||
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet2, disclosureSpec).build();
|
||||
|
||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||
var exception = assertThrows(
|
||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||
.withValidateExpirationClaim(true)
|
||||
|
@ -187,15 +188,15 @@ public abstract class SdJwtVerificationTest {
|
|||
claimSet.put("iat", now + 1000); // issued in the future
|
||||
|
||||
// Exp claim is plain
|
||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||
// Exp claim is undisclosed
|
||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
||||
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet()))
|
||||
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||
.build()).build();
|
||||
|
||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||
var exception = assertThrows(
|
||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||
.withValidateIssuedAtClaim(true)
|
||||
|
@ -216,15 +217,15 @@ public abstract class SdJwtVerificationTest {
|
|||
claimSet.put("nbf", now + 1000); // now will be too soon to accept the jwt
|
||||
|
||||
// Exp claim is plain
|
||||
var sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||
SdJwt sdJwtV1 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder().build()).build();
|
||||
// Exp claim is undisclosed
|
||||
var sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withRedListedClaimNames(DisclosureRedList.of(Set.of()))
|
||||
SdJwt sdJwtV2 = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withRedListedClaimNames(DisclosureRedList.of(Collections.emptySet()))
|
||||
.withUndisclosedClaim("iat", "eluV5Og3gSNII8EYnsxA_A")
|
||||
.build()).build();
|
||||
|
||||
for (SdJwt sdJwt : List.of(sdJwtV1, sdJwtV2)) {
|
||||
var exception = assertThrows(
|
||||
for (SdJwt sdJwt : Arrays.asList(new SdJwt[]{sdJwtV1, sdJwtV2})) {
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||
.withValidateNotBeforeClaim(true)
|
||||
|
@ -242,9 +243,9 @@ public abstract class SdJwtVerificationTest {
|
|||
claimSet.put("given_name", "John");
|
||||
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,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts()
|
||||
.build())
|
||||
|
@ -255,15 +256,15 @@ public abstract class SdJwtVerificationTest {
|
|||
|
||||
@Test
|
||||
public void sdJwtVerificationShouldFail_IfForbiddenClaimNames() {
|
||||
for (String forbiddenClaimName : List.of("_sd", "...")) {
|
||||
for (String forbiddenClaimName : Arrays.asList(new String[]{"_sd", "..."})) {
|
||||
ObjectNode claimSet = mapper.createObjectNode();
|
||||
claimSet.put(forbiddenClaimName, "Value");
|
||||
|
||||
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withUndisclosedClaim(forbiddenClaimName, "eluV5Og3gSNII8EYnsxA_A")
|
||||
.build()).build();
|
||||
|
||||
var exception = assertThrows(
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||
);
|
||||
|
@ -277,13 +278,13 @@ public abstract class SdJwtVerificationTest {
|
|||
ObjectNode claimSet = mapper.createObjectNode();
|
||||
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")
|
||||
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||
.withDecoyClaim("G02NSrQfjFXQ7Io09syajA")
|
||||
.build()).build();
|
||||
|
||||
var exception = assertThrows(
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||
);
|
||||
|
@ -297,14 +298,14 @@ public abstract class SdJwtVerificationTest {
|
|||
claimSet.put("given_name", "John");
|
||||
claimSet.put("family_name", "Doe");
|
||||
|
||||
var salt = "eluV5Og3gSNII8EYnsxA_A";
|
||||
var sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
String salt = "eluV5Og3gSNII8EYnsxA_A";
|
||||
SdJwt sdJwt = exampleFlatSdJwtV2(claimSet, DisclosureSpec.builder()
|
||||
.withUndisclosedClaim("given_name", salt)
|
||||
// We are reusing the same salt value, and that is the problem
|
||||
.withUndisclosedClaim("family_name", salt)
|
||||
.build()).build();
|
||||
|
||||
var exception = assertThrows(
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwt.verify(defaultIssuerSignedJwtVerificationOpts().build())
|
||||
);
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
|
|||
import org.keycloak.sdjwt.vp.SdJwtVP;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
|
@ -69,9 +70,11 @@ public abstract class SdJwtVPVerificationTest {
|
|||
|
||||
@Test
|
||||
public void testVerif_s20_8_sdjwt_with_kb__AltCnfCurves() throws VerificationException {
|
||||
var entries = List.of("sdjwt/s20.8-sdjwt+kb--es384.txt", "sdjwt/s20.8-sdjwt+kb--es512.txt");
|
||||
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);
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||
|
||||
|
@ -84,14 +87,14 @@ public abstract class SdJwtVPVerificationTest {
|
|||
|
||||
@Test
|
||||
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-ps256.txt",
|
||||
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps384.txt",
|
||||
"sdjwt/s20.8-sdjwt+kb--cnf-rsa-ps512.txt"
|
||||
);
|
||||
});
|
||||
|
||||
for (var entry : entries) {
|
||||
for (String entry : entries) {
|
||||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), entry);
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||
|
||||
|
@ -220,7 +223,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
|
||||
@Test
|
||||
public void testShouldFail_IfKbSdHashWrongFormat() {
|
||||
var kbPayload = exampleKbPayload();
|
||||
ObjectNode kbPayload = exampleKbPayload();
|
||||
|
||||
// This hash is not a string
|
||||
kbPayload.set("sd_hash", mapper.valueToTree(1234));
|
||||
|
@ -235,7 +238,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
|
||||
@Test
|
||||
public void testShouldFail_IfKbSdHashInvalid() {
|
||||
var kbPayload = exampleKbPayload();
|
||||
ObjectNode kbPayload = exampleKbPayload();
|
||||
|
||||
// This hash makes no sense
|
||||
kbPayload.put("sd_hash", "c3FmZHFmZGZlZXNkZmZi");
|
||||
|
@ -252,7 +255,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
public void testShouldFail_IfKbIssuedInFuture() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
|
||||
var kbPayload = exampleKbPayload();
|
||||
ObjectNode kbPayload = exampleKbPayload();
|
||||
kbPayload.set("iat", mapper.valueToTree(now + 1000));
|
||||
|
||||
testShouldFailGeneric2(
|
||||
|
@ -267,7 +270,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
public void testShouldFail_IfKbTooOld() {
|
||||
long issuerSignedJwtIat = 1683000000; // same value in test vector
|
||||
|
||||
var kbPayload = exampleKbPayload();
|
||||
ObjectNode kbPayload = exampleKbPayload();
|
||||
// This KB-JWT is then issued more than 60s ago
|
||||
kbPayload.set("iat", mapper.valueToTree(issuerSignedJwtIat - 120));
|
||||
|
||||
|
@ -285,7 +288,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
public void testShouldFail_IfKbExpired() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
|
||||
var kbPayload = exampleKbPayload();
|
||||
ObjectNode kbPayload = exampleKbPayload();
|
||||
kbPayload.set("exp", mapper.valueToTree(now - 1000));
|
||||
|
||||
testShouldFailGeneric2(
|
||||
|
@ -302,7 +305,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
public void testShouldFail_IfKbNotBeforeTimeYet() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
|
||||
var kbPayload = exampleKbPayload();
|
||||
ObjectNode kbPayload = exampleKbPayload();
|
||||
kbPayload.set("nbf", mapper.valueToTree(now + 1000));
|
||||
|
||||
testShouldFailGeneric2(
|
||||
|
@ -321,7 +324,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.8-sdjwt+kb--cnf-is-not-jwk.txt");
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||
|
||||
var exception = assertThrows(
|
||||
UnsupportedOperationException exception = assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> sdJwtVP.verify(
|
||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||
|
@ -363,7 +366,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
String sdJwtVPString = TestUtils.readFileAsString(getClass(), testFilePath);
|
||||
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString);
|
||||
|
||||
var exception = assertThrows(
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwtVP.verify(
|
||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||
|
@ -399,7 +402,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
+ keyBindingJWT.toJws()
|
||||
);
|
||||
|
||||
var exception = assertThrows(
|
||||
VerificationException exception = assertThrows(
|
||||
VerificationException.class,
|
||||
() -> sdJwtVP.verify(
|
||||
defaultIssuerSignedJwtVerificationOpts().build(),
|
||||
|
@ -431,7 +434,7 @@ public abstract class SdJwtVPVerificationTest {
|
|||
}
|
||||
|
||||
private ObjectNode exampleKbPayload() {
|
||||
var payload = mapper.createObjectNode();
|
||||
ObjectNode payload = mapper.createObjectNode();
|
||||
payload.put("nonce", "1234567890");
|
||||
payload.put("aud", "https://verifier.example.org");
|
||||
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.
|
||||
</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>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
|
|
Loading…
Reference in a new issue