diff --git a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java index 3ee487e437..682ac6153e 100644 --- a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java +++ b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java @@ -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); diff --git a/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java b/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java index 47084ae21b..4de91f5f5d 100644 --- a/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java +++ b/core/src/main/java/org/keycloak/jose/jws/crypto/HashUtils.java @@ -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) { diff --git a/services/src/main/java/org/keycloak/crypto/SHAKE256HashProviderFactory.java b/services/src/main/java/org/keycloak/crypto/SHAKE256HashProviderFactory.java new file mode 100644 index 0000000000..229dcc944b --- /dev/null +++ b/services/src/main/java/org/keycloak/crypto/SHAKE256HashProviderFactory.java @@ -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); + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.crypto.HashProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.crypto.HashProviderFactory index 3ac152c712..6980fa61fc 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.crypto.HashProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.crypto.HashProviderFactory @@ -17,4 +17,5 @@ org.keycloak.crypto.SHA256HashProviderFactory org.keycloak.crypto.SHA384HashProviderFactory -org.keycloak.crypto.SHA512HashProviderFactory \ No newline at end of file +org.keycloak.crypto.SHA512HashProviderFactory +org.keycloak.crypto.SHAKE256HashProviderFactory diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index d583f1d7e5..0bd9271238 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -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 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());