Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ public enum Feature {

OPENAPI("OpenAPI specification served at runtime", Type.EXPERIMENTAL, CLIENT_ADMIN_API_V2),

PQC_ML_DSA("Experimental ML-DSA support for JWKs and Signatures", Type.EXPERIMENTAL),

/**
* @see <a href="https://github.com/keycloak/keycloak/issues/37967">Deprecate for removal the Instagram social broker</a>.
*/
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/org/keycloak/crypto/JavaAlgorithm.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public class JavaAlgorithm {
public static final String SHA512 = "SHA-512";
public static final String SHAKE256 = "SHAKE256";

public static final String ML_DSA_44 = "ML-DSA-44";
public static final String ML_DSA_65 = "ML-DSA-65";
public static final String ML_DSA_87 = "ML-DSA-87";

public static String getJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm, null);
}
Expand Down Expand Up @@ -70,6 +74,12 @@ public static String getJavaAlgorithm(String algorithm, String curve) {
return PS384;
case Algorithm.PS512:
return PS512;
case Algorithm.ML_DSA_44:
return ML_DSA_44;
case Algorithm.ML_DSA_65:
return ML_DSA_65;
case Algorithm.ML_DSA_87:
return ML_DSA_87;
case Algorithm.EdDSA:
if (curve != null) {
return curve;
Expand Down Expand Up @@ -142,6 +152,10 @@ public static String getKeyType(String keyAlgorithm) {
case Algorithm.Ed448:
case Algorithm.Ed25519:
return KeyType.OKP;
case Algorithm.ML_DSA_44:
case Algorithm.ML_DSA_65:
case Algorithm.ML_DSA_87:
return KeyType.AKP;
default:
return KeyType.OCT;
}
Expand All @@ -159,6 +173,10 @@ public static boolean isEddsaJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm).contains("Ed");
}

public static boolean isMldsaJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm).startsWith("ML-DSA-");
}

public static boolean isHMACJavaAlgorithm(String algorithm) {
return getJavaAlgorithm(algorithm).contains("HMAC");
}
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ public JWK akp(PublicKey key) {
return k;
}

public JWK akp(PublicKey key, List<X509Certificate> certificates) {
JWK k = akp(key);
if (certificates != null && !certificates.isEmpty()) {
String[] certificateChain = new String[certificates.size()];
for (int i = 0; i < certificates.size(); i++) {
certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i));
}
k.setX509CertificateChain(certificateChain);
}

return k;
}

public JWK rsa(Key key) {
return rsa(key, null, KeyUse.SIG);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum AlgorithmType {
HMAC,
AES,
ECDSA,
EDDSA
EDDSA,
ML_DSA

}
40 changes: 40 additions & 0 deletions core/src/test/java/org/keycloak/jose/jwk/JWKTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,46 @@ public void publicRs256Chain() throws Exception {
verify(data, sign, JavaAlgorithm.RS256, publicKeyFromJwk);
}

@Test
public void publicMldsa65Chain() throws Exception {
KeyPair keyPair = CryptoIntegration.getProvider().getKeyPairGen(JavaAlgorithm.ML_DSA_65).generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
List<X509Certificate> certificates = Arrays.asList(generateV1SelfSignedCertificate(keyPair, "Test"), generateV1SelfSignedCertificate(keyPair, "Intermediate"));

JWK jwk = JWKBuilder.create().kid(KeyUtils.createKeyId(publicKey)).algorithm("ML-DSA-65").akp(publicKey, certificates);

assertNotNull(jwk.getKeyId());
assertEquals("AKP", jwk.getKeyType());
assertEquals("ML-DSA-65", jwk.getAlgorithm());
assertEquals("sig", jwk.getPublicKeyUse());

assertTrue(jwk instanceof AKPPublicJWK);
assertNotNull(((AKPPublicJWK) jwk).getPub());
assertNotNull(jwk.getX509CertificateChain());

String[] expectedChain = new String[certificates.size()];
for (int i = 0; i < certificates.size(); i++) {
expectedChain[i] = PemUtils.encodeCertificate(certificates.get(i));
}

assertArrayEquals(expectedChain, jwk.getX509CertificateChain());
assertNotNull(jwk.getSha1x509Thumbprint());
assertEquals(PemUtils.generateThumbprint(jwk.getX509CertificateChain(), "SHA-1"), jwk.getSha1x509Thumbprint());
assertNotNull(jwk.getSha256x509Thumbprint());
assertEquals(PemUtils.generateThumbprint(jwk.getX509CertificateChain(), "SHA-256"), jwk.getSha256x509Thumbprint());

String jwkJson = JsonSerialization.writeValueAsString(jwk);

PublicKey publicKeyFromJwk = JWKParser.create().parse(jwkJson).toPublicKey();

// Parse
assertArrayEquals(publicKey.getEncoded(), publicKeyFromJwk.getEncoded());

byte[] data = "Some test string".getBytes(StandardCharsets.UTF_8);
byte[] sign = sign(data, JavaAlgorithm.ML_DSA_65, keyPair.getPrivate());
verify(data, sign, JavaAlgorithm.ML_DSA_65, publicKeyFromJwk);
}

private void testPublicEs256(String algorithm) throws Exception {
KeyPairGenerator keyGen = CryptoIntegration.getProvider().getKeyPairGen(KeyType.EC);
SecureRandom randomGen = new SecureRandom();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ private ContentSigner createSigner(PrivateKey privateKey) {
break;
}
case JavaAlgorithm.Ed25519:
case JavaAlgorithm.Ed448: {
case JavaAlgorithm.Ed448:
case JavaAlgorithm.ML_DSA_44:
case JavaAlgorithm.ML_DSA_65:
case JavaAlgorithm.ML_DSA_87: {
signerBuilder = new JcaContentSignerBuilder(privateKey.getAlgorithm())
.setProvider(BouncyIntegration.PROVIDER);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.crypto;

import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;

public class ClientMLDSASignatureVerifierContext extends AsymmetricSignatureVerifierContext {

public ClientMLDSASignatureVerifierContext(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
super(getKey(session, client, input));
}

private static KeyWrapper getKey(KeycloakSession session, ClientModel client, JWSInput input) throws VerificationException {
KeyWrapper key = PublicKeyStorageManager.getClientPublicKeyWrapper(session, client, input);
if (key == null) {
throw new VerificationException("Key not found");
}
if (!KeyType.AKP.equals(key.getType())) {
throw new VerificationException("Key Type is not AKP: " + key.getType());
}
if (key.getAlgorithm() == null) {
// defaults to the algorithm set to the JWS
// validations should be performed prior to verifying signature in case there are restrictions on the algorithms
// that can used for signing
key.setAlgorithm(input.getHeader().getRawAlgorithm());
}
return key;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.crypto;

import org.keycloak.models.KeycloakSession;

public class MLDSA44ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {

public static final String ID = Algorithm.ML_DSA_44;

@Override
public String getId() {
return ID;
}

@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new MLDSAClientSignatureVerifierProvider(session, ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.crypto;

import org.keycloak.models.KeycloakSession;

public class MLDSA44SignatureProviderFactory implements SignatureProviderFactory {

public static final String ID = Algorithm.ML_DSA_44;

@Override
public String getId() {
return ID;
}

@Override
public SignatureProvider create(KeycloakSession session) {
return new MLDSASignatureProvider(session, ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.crypto;

import org.keycloak.models.KeycloakSession;

public class MLDSA65ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {

public static final String ID = Algorithm.ML_DSA_65;

@Override
public String getId() {
return ID;
}

@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new MLDSAClientSignatureVerifierProvider(session, ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2025 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;

public class MLDSA65SignatureProviderFactory implements SignatureProviderFactory {

public static final String ID = Algorithm.ML_DSA_65;

@Override
public String getId() {
return ID;
}

@Override
public SignatureProvider create(KeycloakSession session) {
return new MLDSASignatureProvider(session, ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.crypto;

import org.keycloak.models.KeycloakSession;

public class MLDSA87ClientSignatureVerifierProviderFactory implements ClientSignatureVerifierProviderFactory {

public static final String ID = Algorithm.ML_DSA_87;

@Override
public String getId() {
return ID;
}

@Override
public ClientSignatureVerifierProvider create(KeycloakSession session) {
return new MLDSAClientSignatureVerifierProvider(session, ID);
}
}
Loading