Add SHAKE256 hash provider for Ed448

Closes #31931

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-08-08 13:01:42 +02:00 committed by Marek Posolda
parent 966a454548
commit 2a06e1a6db
5 changed files with 91 additions and 9 deletions

View file

@ -37,7 +37,7 @@ public class JavaAlgorithm {
public static final String SHA256 = "SHA-256"; public static final String SHA256 = "SHA-256";
public static final String SHA384 = "SHA-384"; public static final String SHA384 = "SHA-384";
public static final String SHA512 = "SHA-512"; public static final String SHA512 = "SHA-512";
public static final String SHAKE256 = "SHAKE-256"; public static final String SHAKE256 = "SHAKE256";
public static String getJavaAlgorithm(String algorithm) { public static String getJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm, null); return getJavaAlgorithm(algorithm, null);

View file

@ -34,15 +34,23 @@ public class HashUtils {
// - "at_hash" and "c_hash" in OIDC specification (full = false) // - "at_hash" and "c_hash" in OIDC specification (full = false)
// - "ath" in DPoP specification (full = true) // - "ath" in DPoP specification (full = true)
public static String accessTokenHash(String jwtAlgorithmName, String input, boolean full) { public static String accessTokenHash(String jwtAlgorithmName, String input, boolean full) {
return accessTokenHash(jwtAlgorithmName, null, input, full);
}
public static String accessTokenHash(String jwtAlgorithmName, String curve, String input, boolean full) {
byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName); String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName, curve);
byte[] hash = hash(javaAlgName, inputBytes); byte[] hash = hash(javaAlgName, inputBytes);
return encodeHashToOIDC(hash, full); return encodeHashToOIDC(hash, full);
} }
public static String accessTokenHash(String jwtAlgorithmName, String input) { public static String accessTokenHash(String jwtAlgorithmName, String input) {
return HashUtils.accessTokenHash(jwtAlgorithmName, input, false); return HashUtils.accessTokenHash(jwtAlgorithmName, null, input, false);
}
public static String accessTokenHash(String jwtAlgorithmName, String curve, String input) {
return HashUtils.accessTokenHash(jwtAlgorithmName, curve, input, false);
} }
public static byte[] hash(String javaAlgorithmName, byte[] inputBytes) { public static byte[] hash(String javaAlgorithmName, byte[] inputBytes) {

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.crypto;
import org.keycloak.models.KeycloakSession;
/**
*
* @author rmartinc
*/
public class SHAKE256HashProviderFactory implements HashProviderFactory {
public static final String ID = JavaAlgorithm.SHAKE256;
@Override
public String getId() {
return ID;
}
@Override
public HashProvider create(KeycloakSession session) {
return new JavaAlgorithmHashProvider(ID);
}
}

View file

@ -18,3 +18,4 @@
org.keycloak.crypto.SHA256HashProviderFactory org.keycloak.crypto.SHA256HashProviderFactory
org.keycloak.crypto.SHA384HashProviderFactory org.keycloak.crypto.SHA384HashProviderFactory
org.keycloak.crypto.SHA512HashProviderFactory org.keycloak.crypto.SHA512HashProviderFactory
org.keycloak.crypto.SHAKE256HashProviderFactory

View file

@ -28,7 +28,6 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@ -41,16 +40,20 @@ import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.enums.SslRequired; import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.ECDSAAlgorithm; import org.keycloak.crypto.ECDSAAlgorithm;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyUse;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.jose.jwk.JWK; import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HashUtils;
import org.keycloak.keys.GeneratedEddsaKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -64,6 +67,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -114,7 +118,6 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assume.*;
import static org.keycloak.testsuite.Assert.assertExpiration; import static org.keycloak.testsuite.Assert.assertExpiration;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId; import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
@ -1326,6 +1329,30 @@ public class AccessTokenTest extends AbstractKeycloakTest {
conductAccessTokenRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.EdDSA, Algorithm.EdDSA); conductAccessTokenRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.EdDSA, Algorithm.EdDSA);
} }
@Test
public void accessTokenRequest_ClientEdDSA_RealmEdDSA_Ed448() throws Exception {
// create the generated EdDSA key with Ed448 before performing the test
RealmResource realm = adminClient.realm("test");
ComponentRepresentation comp = new ComponentRepresentation();
comp.setName("eddsa-es448-test");
comp.setProviderId(GeneratedEddsaKeyProviderFactory.ID);
comp.setProviderType(KeyProvider.class.getName());
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.putSingle("eddsaEllipticCurveKey", JavaAlgorithm.Ed448);
config.putSingle("priority", "100");
comp.setConfig(config);
String compId = null;
try (Response response = realm.components().add(comp)) {
assertEquals(201, response.getStatus());
compId = ApiUtil.getCreatedId(response);
conductAccessTokenRequest(Constants.INTERNAL_SIGNATURE_ALGORITHM, Algorithm.EdDSA, Algorithm.EdDSA, JavaAlgorithm.Ed448);
} finally {
if (compId != null) {
realm.components().removeComponent(compId);
}
}
}
@Test @Test
public void validateECDSASignatures() { public void validateECDSASignatures() {
validateTokenECDSASignature(Algorithm.ES256); validateTokenECDSASignature(Algorithm.ES256);
@ -1361,18 +1388,22 @@ public class AccessTokenTest extends AbstractKeycloakTest {
} }
private void conductAccessTokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception { private void conductAccessTokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
conductAccessTokenRequest(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg, null);
}
private void conductAccessTokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg, String idTokenCurve) throws Exception {
try { try {
/// Realm Setting is used for ID Token Signature Algorithm /// Realm Setting is used for ID Token Signature Algorithm
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedIdTokenAlg); TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedIdTokenAlg);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), expectedAccessAlg); TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), expectedAccessAlg);
tokenRequest(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg); tokenRequest(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg, idTokenCurve);
} finally { } finally {
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256); TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256); TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256);
} }
} }
private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception { private void tokenRequest(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg, String idTokenCurve) throws Exception {
oauth.doLogin("test-user@localhost", "password"); oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent(); EventRepresentation loginEvent = events.expectLogin().assertEvent();
@ -1402,6 +1433,9 @@ public class AccessTokenTest extends AbstractKeycloakTest {
assertEquals("JWT", header.getType()); assertEquals("JWT", header.getType());
assertNull(header.getContentType()); assertNull(header.getContentType());
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
assertEquals(idToken.getAccessTokenHash(), HashUtils.accessTokenHash(expectedIdTokenAlg, idTokenCurve, response.getAccessToken()));
AccessToken token = oauth.verifyToken(response.getAccessToken()); AccessToken token = oauth.verifyToken(response.getAccessToken());
assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject()); assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject());