diff --git a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java index 682ac6153ed..6d5be702c1c 100644 --- a/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java +++ b/core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java @@ -130,6 +130,21 @@ public class JavaAlgorithm { } } + public static String getKeyType(String keyAlgorithm) { + switch (keyAlgorithm) { + case KeyType.RSA: + return KeyType.RSA; + case KeyType.EC: + return KeyType.EC; + case Algorithm.EdDSA: + case Algorithm.Ed448: + case Algorithm.Ed25519: + return KeyType.OKP; + default: + return KeyType.OCT; + } + } + public static boolean isRSAJavaAlgorithm(String algorithm) { return getJavaAlgorithm(algorithm).contains("RSA"); } diff --git a/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java index 759842284b2..a23d14fb528 100644 --- a/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java +++ b/core/src/main/java/org/keycloak/protocol/oidc/client/authentication/JWTClientCredentialsProvider.java @@ -73,7 +73,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { keyWrapper.setUse(KeyUse.SIG); // check the algorithm is valid - switch (keyPair.getPublic().getAlgorithm()) { + switch (JavaAlgorithm.getKeyType(keyPair.getPublic().getAlgorithm())) { case KeyType.RSA: if (!JavaAlgorithm.isRSAJavaAlgorithm(algorithm)) { throw new RuntimeException("Invalid algorithm for a RSA KeyPair: " + algorithm); @@ -86,6 +86,12 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider { } this.sigCtx = new ECDSASignatureSignerContext(keyWrapper); break; + case KeyType.OKP: + if (!JavaAlgorithm.isEddsaJavaAlgorithm(algorithm)) { + throw new RuntimeException("Invalid algorithm for a EdDSA KeyPair: " + algorithm); + } + this.sigCtx = new AsymmetricSignatureSignerContext(keyWrapper); + break; default: throw new RuntimeException("Invalid KeyPair algorithm: " + keyPair.getPublic().getAlgorithm()); } diff --git a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java index 8d253c4062f..7c53ee1cc0c 100644 --- a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java +++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java @@ -20,6 +20,8 @@ package org.keycloak.keys.loader; import org.jboss.logging.Logger; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.KeyUtils; +import org.keycloak.crypto.JavaAlgorithm; +import org.keycloak.crypto.KeyType; import org.keycloak.crypto.KeyUse; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.PublicKeysWrapper; @@ -112,16 +114,22 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(clientCert.getPublicKey()); keyWrapper.setKid(kid); keyWrapper.setPublicKey(clientCert.getPublicKey()); - keyWrapper.setType(clientCert.getPublicKey().getAlgorithm()); + keyWrapper.setType(JavaAlgorithm.getKeyType(clientCert.getPublicKey().getAlgorithm())); keyWrapper.setCertificate(clientCert); keyWrapper.setIsDefaultClientCertificate(true); + if (KeyType.OKP.equals(keyWrapper.getType())) { + keyWrapper.setCurve(clientCert.getPublicKey().getAlgorithm()); + } } else { PublicKey publicKey = KeycloakModelUtils.getPublicKey(encodedPublicKey); // Check if we have kid in DB, generate otherwise kid = certInfo.getKid() != null ? certInfo.getKid() : KeyUtils.createKeyId(publicKey); keyWrapper.setKid(kid); keyWrapper.setPublicKey(publicKey); - keyWrapper.setType(publicKey.getAlgorithm()); + keyWrapper.setType(JavaAlgorithm.getKeyType(publicKey.getAlgorithm())); + if (KeyType.OKP.equals(keyWrapper.getType())) { + keyWrapper.setCurve(publicKey.getAlgorithm()); + } } return keyWrapper; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java index 0e49d344ea3..66f85f622cc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AuthzClientCredentialsTest.java @@ -90,6 +90,10 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest { .attribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, "HS512") .authenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID)) .build()); + testRealms.add(configureRealm(RealmBuilder.create().name("authz-client-jwt-test-Ed25519"), ClientBuilder.create() + .attribute(JWTClientAuthenticator.CERTIFICATE_ATTR, "MIH9MIGwoAMCAQICCQDdYD1y8dkE8jAFBgMrZXAwEjEQMA4GA1UEAxMHY2xpZW50NDAgFw0yNTA5MTkwNjIwMjNaGA8yMDc1MDkwNzA2MjAyM1owEjEQMA4GA1UEAxMHY2xpZW50NDAqMAUGAytlcAMhAKjqQuu4BpGsOi0KRwuelXLHW45J6349akYxwadEGordoyEwHzAdBgNVHQ4EFgQUuYEYJBazl+lky1x1tE66zCM3BLUwBQYDK2VwA0EAEOf2nwTiTU+7fAqni3d5N3htu6aCAtUgmPL8VLWlDbj3NpTs7jEIV8oH62Ach/aNFL1ZSrzNEKab19ICxPH8AA==") + .authenticatorType(JWTClientAuthenticator.PROVIDER_ID)) + .build()); testRealms.add(configureRealm(RealmBuilder.create().name("authz-test"), ClientBuilder.create().secret("secret")).build()); testRealms.add(configureRealm(RealmBuilder.create().name("authz-test-session").accessTokenLifespan(1), ClientBuilder.create().secret("secret")).build()); testRealms.add(configureRealm(RealmBuilder.create().name("authz-test-no-rt").accessTokenLifespan(1), ClientBuilder.create().secret("secret").attribute(OIDCConfigAttributes.USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT, "false")).build()); @@ -170,6 +174,11 @@ public class AuthzClientCredentialsTest extends AbstractAuthzTest { testSuccessfulAuthorizationRequest("keycloak-with-jwt-es512-authentication.json"); } + @Test + public void testSuccessfulAuthorizationEd25519Request() throws Exception { + testSuccessfulAuthorizationRequest("keycloak-with-jwt-Ed25519-authentication.json"); + } + @Test public void failJWTAuthentication() { try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-Ed25519-authentication.json b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-Ed25519-authentication.json new file mode 100644 index 00000000000..e7295bf01ff --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/authorization-test/keycloak-with-jwt-Ed25519-authentication.json @@ -0,0 +1,16 @@ +{ + "realm": "authz-client-jwt-test-Ed25519", + "auth-server-url" : "http://localhost:8180/auth", + "resource" : "resource-server-test", + "credentials": { + "jwt": { + "client-keystore-file": "classpath:client-auth-test/keystore-client4.jks", + "client-keystore-type": "JKS", + "client-keystore-password": "storepass", + "client-key-alias": "clientkey", + "client-key-password": "keypass", + "algorithm": "EdDSA", + "token-expiration": 10 + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client4.jks b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client4.jks new file mode 100644 index 00000000000..2d12805abf8 Binary files /dev/null and b/testsuite/integration-arquillian/tests/base/src/test/resources/client-auth-test/keystore-client4.jks differ