Downgrade Java for client libraries to 8

Closes #33051

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-09-19 10:49:42 +02:00 committed by Marek Posolda
parent 71bc313297
commit c532751ff4
19 changed files with 408 additions and 240 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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