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 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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.SHA256HashProviderFactory
|
||||||
org.keycloak.crypto.SHA384HashProviderFactory
|
org.keycloak.crypto.SHA384HashProviderFactory
|
||||||
org.keycloak.crypto.SHA512HashProviderFactory
|
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.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());
|
||||||
|
|
Loading…
Reference in a new issue