diff --git a/core/src/main/java/org/keycloak/crypto/Algorithm.java b/core/src/main/java/org/keycloak/crypto/Algorithm.java
index ab6efb6e2a0..1c7f6aac012 100755
--- a/core/src/main/java/org/keycloak/crypto/Algorithm.java
+++ b/core/src/main/java/org/keycloak/crypto/Algorithm.java
@@ -54,4 +54,8 @@ public interface Algorithm {
String ECDH_ES_A128KW = CryptoConstants.ECDH_ES_A128KW;
String ECDH_ES_A192KW = CryptoConstants.ECDH_ES_A192KW;
String ECDH_ES_A256KW = CryptoConstants.ECDH_ES_A256KW;
+
+ String ML_DSA_44 = "ML-DSA-44";
+ String ML_DSA_65 = "ML-DSA-65";
+ String ML_DSA_87 = "ML-DSA-87";
}
diff --git a/core/src/main/java/org/keycloak/crypto/KeyType.java b/core/src/main/java/org/keycloak/crypto/KeyType.java
index 0de5e376b26..8991d9fd808 100644
--- a/core/src/main/java/org/keycloak/crypto/KeyType.java
+++ b/core/src/main/java/org/keycloak/crypto/KeyType.java
@@ -22,5 +22,6 @@ public interface KeyType {
String RSA = "RSA";
String OCT = "OCT";
String OKP = "OKP";
+ String AKP = "AKP";
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/AKPPublicJWK.java b/core/src/main/java/org/keycloak/jose/jwk/AKPPublicJWK.java
new file mode 100644
index 00000000000..80598b6a3b4
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jwk/AKPPublicJWK.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 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.jose.jwk;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class AKPPublicJWK extends JWK {
+
+ public static final String PUB = "pub";
+
+ @JsonProperty(PUB)
+ private String pub;
+
+ public String getPub() {
+ return pub;
+ }
+
+ public void setPub(String pub) {
+ this.pub = pub;
+ }
+
+ @JsonIgnore
+ @Override
+ public T getOtherClaim(String claimName, Class claimType) {
+ Object claim = null;
+ if (claimName.equals(PUB)) {
+ claim = getPub();
+ }
+ if (claim != null) {
+ return claimType.cast(claim);
+ } else {
+ return super.getOtherClaim(claimName, claimType);
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/AKPUtils.java b/core/src/main/java/org/keycloak/jose/jwk/AKPUtils.java
new file mode 100644
index 00000000000..e1f6fd21e0e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/jose/jwk/AKPUtils.java
@@ -0,0 +1,59 @@
+package org.keycloak.jose.jwk;
+
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.keycloak.crypto.Algorithm;
+
+/**
+ * Adds and removes prefix to X.509 DER encoded public keys.
+ */
+public class AKPUtils {
+
+ // See AKPJWKTest to generate new prefixes
+ static final Map PREFIXES = new HashMap<>();
+ static {
+ PREFIXES.put(Algorithm.ML_DSA_44, new byte[] { 48, -126, 5, 50, 48, 11, 6, 9, 96, -122, 72, 1, 101, 3, 4, 3, 17, 3, -126, 5, 33, 0, });
+ PREFIXES.put(Algorithm.ML_DSA_65, new byte[] { 48, -126, 7, -78, 48, 11, 6, 9, 96, -122, 72, 1, 101, 3, 4, 3, 18, 3, -126, 7, -95, 0, });
+ PREFIXES.put(Algorithm.ML_DSA_87, new byte[] { 48, -126, 10, 50, 48, 11, 6, 9, 96, -122, 72, 1, 101, 3, 4, 3, 19, 3, -126, 10, 33, 0, });
+ }
+
+ public static PublicKey fromEncodedPub(String publicKey, String algorithm) {
+ try {
+ byte[] prefix = PREFIXES.get(algorithm);
+ byte[] keyWithPadding = combine(prefix, Base64.getUrlDecoder().decode(publicKey));
+
+ EncodedKeySpec keySpec = new X509EncodedKeySpec(keyWithPadding);
+ KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
+
+ return keyFactory.generatePublic(keySpec);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String toEncodedPub(PublicKey publicKey, String algorithm) {
+ byte[] prefix = PREFIXES.get(algorithm);
+ byte[] keyOutWithoutPadding = removePadding(publicKey.getEncoded(), prefix.length);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(keyOutWithoutPadding);
+ }
+
+ private static byte[] combine(byte[] first, byte[] second) {
+ byte[] c = new byte[first.length + second.length];
+ System.arraycopy(first, 0, c, 0, first.length);
+ System.arraycopy(second, 0, c, first.length, second.length);
+ return c;
+ }
+
+ private static byte[] removePadding(byte[] bytes, int length) {
+ byte[] b = new byte[bytes.length - length];
+ System.arraycopy(bytes, length, b, 0, bytes.length - length);
+ return b;
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
index 74110b6ae81..12ec0e52c45 100644
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
@@ -83,6 +83,19 @@ public class JWKBuilder {
return rsa(key);
}
+ public JWK akp(PublicKey key) {
+ AKPPublicJWK k = new AKPPublicJWK();
+
+ String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key);
+ k.setKeyId(kid);
+ k.setKeyType(KeyType.AKP);
+ k.setAlgorithm(algorithm);
+ k.setPub(AKPUtils.toEncodedPub(key, algorithm));
+ k.setPublicKeyUse(KeyUse.SIG.getSpecName());
+
+ return k;
+ }
+
public JWK rsa(Key key) {
return rsa(key, null, KeyUse.SIG);
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
index eb6082b29f5..4797b6814c0 100644
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
@@ -82,6 +82,8 @@ public class JWKParser {
return createECPublicKey(normalizedJwkNode);
} else if (KeyType.OKP.equals(keyType)) {
return JWKBuilder.EdEC_UTILS.createOKPPublicKey(jwk);
+ } else if (KeyType.AKP.equals(keyType)) {
+ return createAPKPublicKey(normalizedJwkNode);
} else {
throw new RuntimeException("Unsupported keyType " + keyType);
}
@@ -143,8 +145,15 @@ public class JWKParser {
}
}
+ private static PublicKey createAPKPublicKey(JsonNode jwk) {
+ String algorithm = jwk.path(JWK.ALGORITHM).asText();
+ String publicKey = jwk.path(AKPPublicJWK.PUB).asText();
+ return AKPUtils.fromEncodedPub(publicKey, algorithm);
+ }
+
public boolean isKeyTypeSupported(String keyType) {
return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType)
- || (JWKBuilder.EdEC_UTILS.isEdECSupported() && OKPPublicJWK.OKP.equals(keyType)));
+ || (JWKBuilder.EdEC_UTILS.isEdECSupported() && OKPPublicJWK.OKP.equals(keyType)))
+ || KeyType.AKP.equals(keyType);
}
}
diff --git a/core/src/test/java/org/keycloak/jose/jwk/AKPJWKTest.java b/core/src/test/java/org/keycloak/jose/jwk/AKPJWKTest.java
new file mode 100644
index 00000000000..d10eca600b1
--- /dev/null
+++ b/core/src/test/java/org/keycloak/jose/jwk/AKPJWKTest.java
@@ -0,0 +1,100 @@
+package org.keycloak.jose.jwk;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+
+import org.keycloak.crypto.Algorithm;
+import org.keycloak.crypto.KeyType;
+import org.keycloak.crypto.KeyUse;
+import org.keycloak.rule.CryptoInitRule;
+import org.keycloak.util.JsonSerialization;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+public abstract class AKPJWKTest {
+
+ // There is a chance that two keys are generated starting with the same byte, hence generating multiple keys to take the common prefix from all
+ private static final int KEYS_TO_GENERATE = 5;
+
+ @ClassRule
+ public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
+
+ @Test
+ public void parseMLS_DSA_44() throws IOException {
+ testDecodingAndEncodingPublicKey(Algorithm.ML_DSA_44, AKPSamples.ML_DSA_44);
+ }
+
+ @Test
+ public void parseMLS_DSA_65() throws IOException {
+ testDecodingAndEncodingPublicKey(Algorithm.ML_DSA_65, AKPSamples.ML_DSA_65);
+ }
+
+ @Test
+ public void parseMLS_DSA_87() throws IOException {
+ testDecodingAndEncodingPublicKey(Algorithm.ML_DSA_87, AKPSamples.ML_DSA_87);
+ }
+
+ @Test
+ public void testPrefixMLS_DSA_44() throws NoSuchAlgorithmException {
+ testPrefix(Algorithm.ML_DSA_44);
+ }
+
+ @Test
+ public void testPrefixMLS_DSA_65() throws NoSuchAlgorithmException {
+ testPrefix(Algorithm.ML_DSA_65);
+ }
+
+ @Test
+ public void testPrefixMLS_DSA_87() throws NoSuchAlgorithmException {
+ testPrefix(Algorithm.ML_DSA_87);
+ }
+
+ private void testDecodingAndEncodingPublicKey(String algorithm, String sampleJwk) throws IOException {
+ JWK jwk = JsonSerialization.readValue(sampleJwk, JWK.class);
+
+ PublicKey publicKey = JWKParser.create(jwk).toPublicKey();
+
+ Assert.assertTrue(publicKey.getAlgorithm().startsWith("ML-DSA"));
+
+ JWK akp = JWKBuilder.create().algorithm(algorithm).kid(jwk.getKeyId()).akp(publicKey);
+
+ Assert.assertEquals(algorithm, akp.getAlgorithm());
+ Assert.assertEquals(KeyType.AKP, akp.getKeyType());
+ Assert.assertEquals(KeyUse.SIG.getSpecName(), akp.getPublicKeyUse());
+ Assert.assertEquals(jwk.getKeyId(), akp.getKeyId());
+ Assert.assertEquals(jwk.getOtherClaim(AKPPublicJWK.PUB, String.class), akp.getOtherClaim(AKPPublicJWK.PUB, String.class));
+ }
+
+ private JWK getJwk(String algorithm) throws IOException {
+ InputStream inputStream = getClass().getResourceAsStream(algorithm.replace('-', '_') + ".jose.json");
+ return JsonSerialization.readValue(inputStream, JWK.class);
+ }
+
+ private void testPrefix(String algorithm) throws NoSuchAlgorithmException {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance(algorithm);
+
+ byte[] expectedPrefix = kpg.generateKeyPair().getPublic().getEncoded();
+
+ for (int i = 0; i < KEYS_TO_GENERATE; i++) {
+ byte[] bytes2 = kpg.generateKeyPair().getPublic().getEncoded();
+ expectedPrefix = findMatchingPrefix(expectedPrefix, bytes2);
+ }
+
+ Assert.assertArrayEquals(AKPUtils.PREFIXES.get(algorithm), expectedPrefix);
+ }
+
+ private static byte[] findMatchingPrefix(byte[] a, byte[] b) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ for (int i = 0; i < a.length && a[i] == b[i]; i++) {
+ bos.write(a[i]);
+ }
+ return bos.toByteArray();
+ }
+
+}
diff --git a/core/src/test/java/org/keycloak/jose/jwk/AKPSamples.java b/core/src/test/java/org/keycloak/jose/jwk/AKPSamples.java
new file mode 100644
index 00000000000..8b9ff40fc17
--- /dev/null
+++ b/core/src/test/java/org/keycloak/jose/jwk/AKPSamples.java
@@ -0,0 +1,30 @@
+package org.keycloak.jose.jwk;
+
+// ML-DSA examples from https://github.com/cose-wg/draft-ietf-cose-dilithium/tree/main/examples/jose
+public interface AKPSamples {
+
+ String ML_DSA_44 = "{\n" +
+ " \"kid\": \"T4xl70S7MT6Zeq6r9V9fPJGVn76wfnXJ21-gyo0Gu6o\",\n" +
+ " \"kty\": \"AKP\",\n" +
+ " \"alg\": \"ML-DSA-44\",\n" +
+ " \"pub\": \"unH59k4RuutY-pxvu24U5h8YZD2rSVtHU5qRZsoBmBMcRPgmu9VuNOVdteXi1zNIXjnqJg_GAAxepLqA00Vc3lO0bzRIKu39VFD8Lhuk8l0V-cFEJC-zm7UihxiQMMUEmOFxe3x1ixkKZ0jqmqP3rKryx8tSbtcXyfea64QhT6XNje2SoMP6FViBDxLHBQo2dwjRls0k5a-XSQSu2OTOiHLoaWsLe8pQ5FLNfTDqmkrawDEdZyxr3oSWJAsHQxRjcIiVzZuvwxYy1zl2STiP2vy_fTBaPemkleynQzqPg7oPCyXEE8bjnJbrfWkbNNN8438e6tHPIX4l7zTuzz98YPhLjt_d6EBdT4MldsYe-Y4KLyjaGHcAlTkk9oa5RhRwW89T0z_t1DSO3dvfKLUGXh8gd1BD6Fz5MfgpF5NjoafnQEqDjsAAhrCXY4b-Y3yYJEdX4_dp3dRGdHG_rWcPmgX4JG7lCnser4f8QGnDriqiAzJYEXeS8LzUngg_0bx0lqv_KcyU5IaLISFO0xZSU5mmEPvdSoDnyAcV8pV44qhLtAvd29n0ehG259oRihtljTWeiu9V60a1N2tbZVl5mEqSK-6_xZvNYA1TCdzNctvweH24unV7U3wer9XA9Q6kvJWDVJ4oKaQsKMrCSMlteBJMRxWbGK7ddUq6F7GdQw-3j2M-qdJvVKm9UPjY9rc1lPgol25-oJxTu7nxGlbJUH-4m5pevAN6NyZ6lfhbjWTKlxkrEKZvQXs_Yf6cpXEwpI_ZJeriq1UC1XHIpRkDwdOY9MH3an4RdDl2r9vGl_IwlKPNdh_5aF3jLgn7PCit1FNJAwC8fIncAXgAlgcXIpRXdfJk4bBiO89GGccSyDh2EgXYdpG3XvNgGWy7npuSoNTE7WIyblAk13UQuO4sdCbMIuriCdyfE73mvwj15xgb07RZRQtFGlFTmnFcIdZ90zDrWXDbANntv7KCKwNvoTuv64bY3HiGbj-NQ-U9eMylWVpvr4hrXcES8c9K3PqHWADZC0iIOvlzFv4VBoc_wVflcOrL_SIoaNFCNBAZZq-2v5lAgpJTqVOtqJ_HVraoSfcKy5g45p-qULunXj6Jwq21fobQiKubBKKOZwcJFyJD7F4ACKXOrz-HIvSHMCWW_9dVrRuCpJw0s0aVFbRqopDNhu446nqb4_EDYQM1tTHMozPd_jKxRRD0sH75X8ZoToxFSpLBDbtdWcenxj-zBf6IGWfZnmaetjKEBYJWC7QDQx1A91pJVJCEgieCkoIfTqkeQuePpIyu48g2FG3P1zjRF-kumhUTfSjo5qS0YiZQy0E1BMs6M11EvuxXRsHClLHoy5nLYI2Sj4zjVjYyxSHyPRPGGo9hwB34yWxzYNtPPGiqXS_dNCpi_zRZwRY4lCGrQ-hYTEWIK1Dm5OlttvC4_eiQ1dv63NiGkLRJ5kJA3bICN0fzCDY-MBqnd1cWn8YVBijVkgtaoascjL9EywDgJdeHnXK0eeOvUxHHhXJVkNqcibn8O4RQdpVU60TSA-uiu675ytIjcBHC6kTv8A8pmkj_4oypPd-F92YIJC741swkYQoeIHj8rE-ThcMUkF7KqC5VORbZTRp8HsZSqgiJcIPaouuxd1-8Rxrid3fXkE6p8bkrysPYoxWEJgh7ZFsRCPDWX-yTeJwFN0PKFP1j0F6YtlLfK5wv-c4F8ZQHA_-yc_gODicy7KmWDZgbTP07e7gEWzw4MFRrndjbDQ\",\n" +
+ " \"priv\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n" +
+ "}";
+
+ String ML_DSA_65 = "{\n" +
+ " \"kid\": \"Suiu29qbfuaBaR4Ats-c6XQBePB_OpAxAwcTR_0KXVM\",\n" +
+ " \"kty\": \"AKP\",\n" +
+ " \"alg\": \"ML-DSA-65\",\n" +
+ " \"pub\": \"QksvJn5Y1bO0TXGs_Gpla7JpUNV8YdsciAvPof6rRD8JQquL2619cIq7w1YHj22ZolInH-YsdAkeuUr7m5JkxQqIjg3-2AzV-yy9NmfmDVOevkSTAhnNT67RXbs0VaJkgCufSbzkLudVD-_91GQqVa3mk4aKRgy-wD9PyZpOMLzP-opHXlOVOWZ067galJN1h4gPbb0nvxxPWp7kPN2LDlOzt_tJxzrfvC1PjFQwNSDCm_l-Ju5X2zQtlXyJOTZSLQlCtB2C7jdyoAVwrftUXBFDkisElvgmoKlwBks23fU0tfjhwc0LVWXqhGtFQx8GGBQ-zol3e7P2EXmtIClf4KbgYq5u7Lwu848qwaItyTt7EmM2IjxVth64wHlVQruy3GXnIurcaGb_qWg764qZmteoPl5uAWwuTDX292Sa071S7GfsHFxue5lydxIYvpVUu6dyfwuExEubCovYMfz_LJd5zNTKMMatdbBJg-Qd6JPuXznqc1UYC3CccEXCLTOgg_auB6EUdG0b_cy-5bkEOHm7Wi4SDipGNig_ShzUkkot5qSqPZnd2I9IqqToi_0ep2nYLBB3ny3teW21Qpccoom3aGPt5Zl7fpzhg7Q8zsJ4sQ2SuHRCzgQ1uxYlFx21VUtHAjnFDSoMOkGyo4gH2wcLR7-z59EPPNl51pljyNefgCnMSkjrBPyz1wiET-uqi23f8Bq2TVk1jmUFxOwdfLsU7SIS30WOzvwD_gMDexUFpMlEQyL1-Y36kaTLjEWGCi2tx1FTULttQx5JpryPW6lW5oKw5RMyGpfRliYCiRyQePYqipZGoxOHpvCWhCZIN4meDY7H0RxWWQEpiyCzRQgWkOtMViwao6Jb7wZWbLNMebwLJeQJXWunk-gTEeQaMykVJobwDUiX-E_E7fSybVRTZXherY1jrvZKh8C5Gi5VADg5Vs319uN8-dVILRyOOlvjjxclmsRcn6HEvTvxd9MS7lKm2gI8BXIqhzgnTdqNGwTpmDHPV8hygqJWxWXCltBSSgY6OkGkioMAmXjZjYq_Ya9o6AE7WU_hUdm-wZmQLExwtJWEIBdDxrUxA9L9JL3weNyQtaGItPjXcheZiNBBbJTUxXwIYLnXtT1M0mHzMqGFFWXVKsN_AIdHyv4yDzY9m-tuQRfbQ_2K7r5eDOL1Tj8DZ-s8yXG74MMBqOUvlglJNgNcbuPKLRPbSDoN0E3BYkfeDgiUrXy34a5-vU-PkAWCsgAh539wJUUBxqw90V1Du7eTHFKDJEMSFYwusbPhEX4ZTwoeTHg--8Ysn4HCFWLQ00pfBCteqvMvMflcWwVfTnogcPsJb1bEFVSc3nTzhk6Ln8J-MplyS0Y5mGBEtVko_WlyeFsoDCWj4hqrgU7L-ww8vsCRSQfskH8lodiLzj0xmugiKjWUXbYq98x1zSnB9dmPy5P3UNwwMQdpebtR38N9I-jup4Bzok0-JsaOe7EORZ8ld7kAgDWa4K7BAxjc2eD540Apwxs-VLGFVkXbQgYYeDNG2tW1Xt20-XezJqZVUl6-IZXsqc7DijwNInO3fT5o8ZAcLKUUlzSlEXe8sIlHaxjLoJ-oubRtlKKUbzWOHeyxmYZSxYqQhSQj4sheedGXJEYWJ-Y5DRqB-xpy-cftxL10fdXIUhe1hWFBAoQU3b5xRY8KCytYnfLhsFF4O49xhnax3vuumLpJbCqTXpLureoKg5PvWfnpFPB0P-ZWQN35mBzqbb3ZV6U0rU55DvyXTuiZOK2Z1TxbaAd1OZMmg0cpuzewgueV-Nh_UubIqNto5RXCd7vqgqdXDUKAiWyYegYIkD4wbGMqIjxV8Oo2ggOcSj9UQPS1rD5u0rLckAzsxyty9Q5JsmKa0w8Eh7Jwe4Yob4xPVWWbJfm916avRgzDxXo5gmY7txdGFYHhlolJKdhBU9h6f0gtKEtbiUzhp4IWsqAR8riHQs7lLVEz6P537a4kL1r5FjfDf_yjJDBQmy_kdWMDqaNln-MlKK8eENjUO-qZGy0Ql4bMZtNbHXjfJUuSzapA-RqYfkqSLKgQUOW8NTDKhUk73yqCU3TQqDEKaGAoTsPscyMm7u_8QrvUK8kbc-XnxrWZ0BZJBjdinzh2w-QvjbWQ5mqFp4OMgY94__tIU8vvCUNJiYA1RdyodlfPfH5-avpxOCvBD6C7ZIDyQ-6huGEQEAb6DP8ydWIZQ8xY603DoEKKXkJWcP6CJo3nHFEdj_vcEbDQ-WESDpcQFa1fRIiGuALj-sEWcjGdSHyE8QATOcuWl4TLVzRPKAf4tCXx1zyvhJbXQu0jf0yfzVpOhPun4n-xqK4SxPBCeuJOkQ2VG9jDXWH4pnjbAcrqjveJqVti7huMXTLGuqU2uoihBw6mGqu_WSlOP2-XTEyRyvxbv2t-z9V6GPt1V9ceBukA0oGwtJqgD-q7NXFK8zhw7desI5PZMXf3nuVgbJ3xdvAlzkmm5f9RoqQS6_hqwPQEcclq1MEZ3yML5hc99TDtZWy9gGkhR0Hs3QJxxgP7bEqGFP-HjTPnJsrGaT6TjKP7qCxJlcFKLUr5AU_kxMULeUysWWtSGJ9mpxBvsyW1Juo\",\n" +
+ " \"priv\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n" +
+ "}";
+
+ String ML_DSA_87 = "{\n" +
+ " \"kid\": \"tRn1JNIkgMsABVQBlXeDHxAIcclh-2IX0UdDEzPt5XU\",\n" +
+ " \"kty\": \"AKP\",\n" +
+ " \"alg\": \"ML-DSA-87\",\n" +
+ " \"pub\": \"5F_8jMc9uIXcZi5ioYzY44AylxF_pWWIFKmFtf8dt7Roz8gruSnx2Gt37RT1rhamU2h3LOUZEkEBBeBFaXWukf22Q7US8STV5gvWi4x-Mf4Bx7DcZa5HBQHMVlpuHfz8_RJWVDPEr-3VEYIeLpYQxFJ14oNt7jXO1p1--mcv0eQxi-9etuiX6LRRqiAt7QQrKq73envj9pkUbaIpqL2z_6SWRFln51IXv7yQSPmVZEPYcx-DPrMN4Q2slv_-fPZeoERcPjHoYB4TO-ahAHZP4xluJncmRB8xdR-_mm9YgGRPTnJ15X3isPEF5NsFXVDdHJyTT931NbjeKLDHTARJ8iLNLtC7j7x3XM7oyUBmW0D3EvT34AdQ6eHkzZz_JdGUXD6bylPM1PEu7nWBhW69aPJoRZVuPnvrdh8P51vdMb_i-gGBEzl7OHvVnWKmi4r3-iRauTLmn3eOLO79ITBPu4CZ6hPY6lfBgTGXovda4lEHW1Ha04-FNmnp1fmKNlUJiUGZOhWUhg-6cf5TDuXCn1jyl4r2iMy3Wlg4o1nBEumOJahYOsjawfhh_Vjir7pd5aUuAgkE9bQrwIdONb788-YRloR2jzbgCPBHEhd86-YnYHOB5W6q7hYcFym43lHb3kdNSMxoJJ6icWK4eZPmDITtbMZCPLNnbZ61CyyrWjoEnvExOB1iP6b7y8nbHnzAJeoEGLna0sxszU6V-izsJP7spwMYp1Fxa3IT9j7b9lpjM4NX-Dj5TsBxgiwkhRJIiFEHs9HE6SRnjHYU6hrwOBBGGfKuNylAvs-mninLtf9sPiCke-Sk90usNMEzwApqcGrMxv_T2OT71pqZcE4Sg8hQ2MWNHldTzZWHuDxMNGy5pYE3IT7BCDTGat_iu1xQGo7y7K3Rtnej3xpt64br8HIsT1Aw4g-QGN1bb8U-6iT9kre1tAJf6umW0-SP1MZQ2C261-r5NmOWmFEvJiU9LvaEfIUY6FZcyaVJXG__V83nMjiCxUp9tHCrLa-P_Sv3lPp8aS2ef71TLuzB14gOLKCzIWEovii0qfHRUfrJeAiwvZi3tDphKprIZYEr_qxvR0YCd4QLUqOwh_kWynztwPdo6ivRnqIRVfhLSgTEAArSrgWHFU1WC8Ckd6T5MpqJhN0x6x8qBePZGHAdYwz8qa9h7wiNLFWBrLRj5DmQLl1CVxnpVrjW33MFso4P8n060N4ghdKSSZsZozkNQ5b7O6yajYy-rSp6QpD8msb8oEX5imFKRaOcviQ2D4TRT45HJxKs63Tb9FtT1JoORzfkdv_E1bL3zSR6oYbTt2Stnpz-7kVqc8KR2N45EkFKxDkRw3IXOte0cq81xoU87S_ntf4KiVZaszuqb2XN2SgxnXBl4EDnpehPmqkD92SAlLrQcTaxaSe47G28K-8MwoVt4eeVkj4UEsSfJN7rbCH2yKl2XJx5huDaS0xn2ODQyNRmgk-5I9hXMUiZDNLvEzx4zuyrcu2d0oXFo3ZoUtVFNCB__TQCf2x27ej9GjLXLDAEi7qnl9Xfb94n0IfeVyGte3-j6NP3DWv8OrLiUjNTaLv6Fay1yzfUaU6LI86-Jd6ckloiGhg7kE0_hd-ZKakZxU1vh0Vzc6DW7MFAPky75iCZlDXoBpZjTNGo5HR-mCW_ozblu60U9zZA8bn-voANuu_hYwxh-uY1sHTFZOqp2xicnnMChz_GTm1Je8XCkICYegeiHUryEHA6T6B_L9gW8S_R4ptMD0Sv6b1KHqqKeubwKltCWPUsr2En9iYypnz06DEL5Wp8KMhrLid2AMPpLI0j1CWGJExXHpBWjfIC8vbYH4YKVl-euRo8eDcuKosb5hxUGM9Jvy1siVXUpIKpkZt2YLP5pEBP_EVOoHPh5LJomrLMpORr1wBKbEkfom7npX1g817bK4IeYmZELI8zXUUtUkx3LgNTckwjx90Vt6oVXpFEICIUDF_LAVMUftzz6JUvbwOZo8iAZqcnVslAmRXeY_ZPp5eEHFfHlsb8VQ73Rd_p8XlFf5R1WuWiUGp2TzJ-VQvj3BTdQfOwSxR9RUk4xjqNabLqTFcQ7As246bHJXH6XVnd4DbEIDPfNa8FaWb_DNEgQAiXGqa6n7l7aFq5_6Kp0XeBBM0sOzJt4fy8JC6U0DEcMnWxKFDtMM7q06LubQYFCEEdQ5b1Qh2LbQZ898tegmeF--EZ4F4hvYebZPV8sM0ZcsKBXyCr585qs00PRxr0S6rReekGRBIvXzMojmid3dxc6DPpdV3x5zxlxaIBxO3i_6axknSSdxnS04_bemWqQ3CLf6mpSqfTIQJT1407GB4QINAAC9Ch3AXUR_n1jr64TGWzbIr8uDcnoVCJlOgmlXpmOwubigAzJattbWRi7k4QYBnA3_4QMjt73n2Co4-F_Qh4boYLpmwWG2SwcIw2PeXGr2LY2zwkPR4bcSyx1Z6UK5trQpWlpQCxgsvV_RvGzpN22RtHoihPH74K0cBIzCz7tK-jqeuWl1A7af7KmQ66fpRBr5ykTLOsa17WblkcIB_jDvqKfEcdxhPWJUwmOo4TIQS-xH8arLOy_NQFG2m14_yxwUemXC-QxLUYi6_FIcqwPBKjCdpQtadRdyftQSKO0SP-GxUvamMZzWI780rXuOBkq5kyYLy9QF9bf_-bL6QLpe1WMCQlOeXZaCPoncgYoT0WZ17jB52Xb2lPWsyXYK54npszkbKJ4OIqfvF8xqRXcVe22VwJuqT9Uy4-4KKQgQ7TXla7Gdm2H7mKl8YXQlsGCT2Ypc8O4t0Sfw7qYAuaDGf752Hbm3fl1bupcB2huIPlIaDP6IRR9XvTYIW2flbwYfhKLmoVKnG85uUi2qtqCjPOIuU3-peT0othfmwKQXaoOqO-V4r6wPL1VHxVFtIYmEdVt0RccUOvpOVR_OAHG9uHOzTmueK5557Qxp0ojtZCHyN-hgoMZJLrvdKkTCxPNo2-mZQbHoVh2FnThZ9JbO49dB8lKXP4_MU5xAnjXMgKXtbfI8w6ZWATE_XWgf2VQMUpGp4wpy44yWQTxHxh_4T9540BGwG0FU0bkgrwA_erseGZnepqdmz5_ScCs84O5Xr5MbYhJLCGGxY6O5GqS-ooB2w0Mt87KbbE4bpYje9CAHH8FX3pDrJyLsyasA3zxmk4OmGpG7Z70ofONJtHRe56R5287vFmuazEEutXn81kNzB-3aJT1ga3vnWZw4CSvFKoWYSA7auLgrHSHFZdITfOrgtmQmGbFhM9kSBdY1UCnpzf65oos3PZWRa2twfUxxLAnPNtrxpRGyvtsapw7ljUagZmuyh3hLCjhAxYmnoE1dbyIWvpCqSlEtVjL1yb_nuLEzgvmZuV02fHxGuWgHTOMVGXpf81Rce3eoBK3lapW1wkzezlk3tcA2bZOtA9qbxdsbVR37kemzQ9K1e3Y0OWhtSj\",\n" +
+ " \"priv\": \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\n" +
+ "}";
+
+}
diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultCryptoAKPJWKTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultCryptoAKPJWKTest.java
new file mode 100644
index 00000000000..021ef3adeda
--- /dev/null
+++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/DefaultCryptoAKPJWKTest.java
@@ -0,0 +1,16 @@
+package org.keycloak.crypto.def.test;
+
+import org.keycloak.common.util.Environment;
+import org.keycloak.jose.jwk.AKPJWKTest;
+
+import org.junit.Assume;
+import org.junit.Before;
+
+public class DefaultCryptoAKPJWKTest extends AKPJWKTest {
+
+ @Before
+ public void before() {
+ Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
+ }
+
+}