Add SHAKE256 hash provider for Ed448
Closes #31931 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
966a454548
commit
2a06e1a6db
5 changed files with 91 additions and 9 deletions
|
@ -37,7 +37,7 @@ public class JavaAlgorithm {
|
|||
public static final String SHA256 = "SHA-256";
|
||||
public static final String SHA384 = "SHA-384";
|
||||
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) {
|
||||
return getJavaAlgorithm(algorithm, null);
|
||||
|
|
|
@ -34,15 +34,23 @@ public class HashUtils {
|
|||
// - "at_hash" and "c_hash" in OIDC specification (full = false)
|
||||
// - "ath" in DPoP specification (full = true)
|
||||
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);
|
||||
String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName);
|
||||
String javaAlgName = JavaAlgorithm.getJavaAlgorithmForHash(jwtAlgorithmName, curve);
|
||||
byte[] hash = hash(javaAlgName, inputBytes);
|
||||
|
||||
return encodeHashToOIDC(hash, full);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -18,3 +18,4 @@
|
|||
org.keycloak.crypto.SHA256HashProviderFactory
|
||||
org.keycloak.crypto.SHA384HashProviderFactory
|
||||
org.keycloak.crypto.SHA512HashProviderFactory
|
||||
org.keycloak.crypto.SHAKE256HashProviderFactory
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.apache.http.impl.client.CloseableHttpClient;
|
|||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
@ -41,16 +40,20 @@ import org.keycloak.admin.client.resource.UserResource;
|
|||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.crypto.ECDSAAlgorithm;
|
||||
import org.keycloak.crypto.JavaAlgorithm;
|
||||
import org.keycloak.crypto.KeyUse;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.infinispan.util.InfinispanUtils;
|
||||
import org.keycloak.jose.jwk.JWK;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
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.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
@ -64,6 +67,7 @@ import org.keycloak.representations.AccessToken;
|
|||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
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.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assume.*;
|
||||
import static org.keycloak.testsuite.Assert.assertExpiration;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
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);
|
||||
}
|
||||
|
||||
@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
|
||||
public void validateECDSASignatures() {
|
||||
validateTokenECDSASignature(Algorithm.ES256);
|
||||
|
@ -1361,18 +1388,22 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
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 {
|
||||
/// Realm Setting is used for ID Token Signature Algorithm
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedIdTokenAlg);
|
||||
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), expectedAccessAlg);
|
||||
tokenRequest(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg);
|
||||
tokenRequest(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg, idTokenCurve);
|
||||
} finally {
|
||||
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, 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");
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
@ -1402,6 +1433,9 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
assertEquals("JWT", header.getType());
|
||||
assertNull(header.getContentType());
|
||||
|
||||
IDToken idToken = oauth.verifyIDToken(response.getIdToken());
|
||||
assertEquals(idToken.getAccessTokenHash(), HashUtils.accessTokenHash(expectedIdTokenAlg, idTokenCurve, response.getAccessToken()));
|
||||
|
||||
AccessToken token = oauth.verifyToken(response.getAccessToken());
|
||||
|
||||
assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject());
|
||||
|
|
Loading…
Reference in a new issue