8000 Add generated keys provider for ML-DSA by stianst · Pull Request #55099 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
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 @@ -113,6 +113,8 @@ public enum Feature {

KUBERNETES_SERVICE_ACCOUNTS("Kubernetes service accounts trust relationship provider", Type.DEFAULT),

OAUTH_ML_DSA("ML-DSA support for OAuth 2.0 and OpenID Connect", Type.EXPERIMENTAL),

// Check if kerberos is available in underlying JVM and auto-detect if feature should be enabled or disabled by default based on that
KERBEROS("Kerberos", Type.DEFAULT, 1, () -> KerberosJdkProvider.getProvider().isKerberosAvailable()),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ public ProviderConfigProperty(String name, String label, String helpText, String
this.options = Arrays.asList(options);
}

public ProviderConfigProperty(String name, String label, String helpText, String type, Object defaultValue, List<String> options) {
this.name = name;
this.label = label;
this.helpText = helpText;
this.type = type;
this.defaultValue = defaultValue;
this.options = options;
}

public ProviderConfigProperty(String name, String label, String helpText, String type, Object defaultValue, boolean secret) {
this(name, label, helpText, type, defaultValue);
this.secret = secret;
Expand Down
2 changes: 2 additions & 0 deletions services/src/main/java/org/keycloak/keys/Attributes.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public interface Attributes {
String PRIVATE_KEY_KEY = "privateKey";
ProviderConfigProperty PRIVATE_KEY_PROPERTY = new ProviderConfigProperty(PRIVATE_KEY_KEY, "Private RSA Key", "Private RSA Key encoded in PEM format", FILE_TYPE, null, true);

String PUBLIC_KEY_KEY = "publicKey";

String CERTIFICATE_KEY = "certificate";
ProviderConfigProperty CERTIFICATE_PROPERTY = new ProviderConfigProperty(CERTIFICATE_KEY, "X509 Certificate", "X509 Certificate encoded in PEM format", FILE_TYPE, null);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.keycloak.keys;

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.stream.Stream;

import org.keycloak.common.util.KeyUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;

import org.jboss.logging.Logger;

public class GeneratedMlDsaKeyProvider implements KeyProvider {

private static final Logger logger = Logger.getLogger(GeneratedMlDsaKeyProvider.class);

private final KeyStatus status;

private final ComponentModel model;

private final KeyWrapper key;

public GeneratedMlDsaKeyProvider(ComponentModel model) {
this.model = model;
this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true));

if (model.hasNote(KeyWrapper.class.getName())) {
key = model.getNote(KeyWrapper.class.getName());
} else {
key = loadKey(model);
model.setNote(KeyWrapper.class.getName(), key);
}
}

protected KeyWrapper loadKey(ComponentModel model) {
String privateMlDsaKeyBase64Encoded = model.getConfig().getFirst(Attributes.PRIVATE_KEY_KEY);
String publicMlDsaKeyBase64Encoded = model.getConfig().getFirst(Attributes.PUBLIC_KEY_KEY);
String algorithm = model.getConfig().getFirst(Attributes.ALGORITHM_KEY);

try {
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.getMimeDecoder().decode(privateMlDsaKeyBase64Encoded));
KeyFactory kf = KeyFactory.getInstance(algorithm);
PrivateKey decodedPrivateKey = kf.generatePrivate(privateKeySpec);

X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getMimeDecoder().decode(publicMlDsaKeyBase64Encoded));
PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec);

KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey);

return createKeyWrapper(keyPair, algorithm);
} catch (Exception e) {
logger.warnv(e,"Failed to load stored keys for realm={0}, componentId={1}", model.getParentId(), model.getId());
return null;
}
}

@Override
public Stream<KeyWrapper> getKeysStream() {
return Stream.of(key);
}

protected KeyWrapper createKeyWrapper(KeyPair keyPair, String algorithm) {
KeyWrapper key = new KeyWrapper();

key.setProviderId(model.getId());
key.setProviderPriority(model.get("priority", 0L));

key.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
key.setUse(KeyUse.SIG);
key.setType(KeyType.AKP);
key.setAlgorithm(algorithm);
key.setStatus(status);
key.setPrivateKey(keyPair.getPrivate());
key.setPublicKey(keyPair.getPublic());

return key;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package org.keycloak.keys;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyUse;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;

import org.jboss.logging.Logger;

public class GeneratedMlDsaKeyProviderFactory implements KeyProviderFactory<GeneratedMlDsaKeyProvider>, EnvironmentDependentProviderFactory {

private static final Logger logger = Logger.getLogger(GeneratedMlDsaKeyProviderFactory.class);

public static final String ID = "mldsa-generated";

private static final String HELP_TEXT = "Generates ML-DSA keys";

private static final String DEFAULT_ALGORITHM = Algorithm.ML_DSA_65;

public static final Set<String> SUPPORTED_ALGORITHMS = Set.of(
Algorithm.ML_DSA_44, Algorithm.ML_DSA_65, Algorithm.ML_DSA_87);

private static final ProviderConfigProperty ALGORITHM_PROPERTY = new ProviderConfigProperty(Attributes.ALGORITHM_KEY, "Algorithm", "Intended algorithm for the key", ProviderConfigProperty.LIST_TYPE, DEFAULT_ALGORITHM, new LinkedList<>(SUPPORTED_ALGORITHMS));

private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
.property(Attributes.PRIORITY_PROPERTY)
.property(Attributes.ENABLED_PROPERTY)
.property(Attributes.ACTIVE_PROPERTY)
.property(ALGORITHM_PROPERTY)
.build();

@Override
public GeneratedMlDsaKeyProvider create(KeycloakSession session, ComponentModel model) {
return new GeneratedMlDsaKeyProvider(model);
}

@Override
public boolean createFallbackKeys(KeycloakSession session, KeyUse keyUse, String algorithm) {
if (keyUse.equals(KeyUse.SIG) && SUPPORTED_ALGORITHMS.contains(algorithm)) {
RealmModel realm = session.getContext().getRealm();

ComponentModel generated = new ComponentModel();
generated.setName("fallback-" + algorithm);
generated.setParentId(realm.getId());
generated.setProviderId(ID);
generated.setProviderType(KeyProvider.class.getName());

MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
config.putSingle(Attributes.PRIORITY_KEY, "-100");
config.putSingle(Attributes.ALGORITHM_KEY, algorithm);
generated.setConfig(config);

realm.addComponentModel(generated);

return true;
} else {
return false;
}
}

@Override
public String getHelpText() {
return HELP_TEXT;
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return CONFIG_PROPERTIES;
}

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

@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
if (model.getId() != null) {
ComponentModel current = realm.getComponent(model.getId());
if (current != null) {
if (!current.get(Attributes.ALGORITHM_KEY).equals(model.get(Attributes.ALGORITHM_KEY))) {
throw new ComponentValidationException("Changing algorithm is not supported");
}
}
}

ConfigurationValidationHelper.check(model)
.checkLong(Attributes.PRIORITY_PROPERTY, false)
.checkBoolean(Attributes.ENABLED_PROPERTY, false)
.checkBoolean(Attributes.ACTIVE_PROPERTY, false)
.checkList(ALGORITHM_PROPERTY, false);

String algorithm = model.get(Attributes.ALGORITHM_KEY, DEFAULT_ALGORITHM);

if (!(model.contains(Attributes.PRIVATE_KEY_KEY) && model.contains(Attributes.PUBLIC_KEY_KEY))) {
generateKeys(model, algorithm);
logger.debugv("Generated keys for {0}", realm.getName());
}
}

private static void generateKeys(ComponentModel model, String algorithm) {
KeyPair keyPair;
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
keyPair = keyGen.generateKeyPair();
model.put(Attributes.PRIVATE_KEY_KEY, Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
model.put(Attributes.PUBLIC_KEY_KEY, Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
model.put(Attributes.ALGORITHM_KEY, algorithm);
} catch (Throwable t) {
throw new ComponentValidationException("Failed to generate ML-DSA keys", t);
}
}

@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.OAUTH_ML_DSA);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.keycloak.protocol.oidc.utils;

import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -62,6 +63,8 @@ public static JWK toJwk(KeyWrapper key) {
return b.ec(key.getPublicKey(), certificates, key.getUse());
} else if (key.getType().equals(KeyType.OKP)) {
return b.okp(key.getPublicKey(), key.getUse());
} else if (key.getType().equals(KeyType.AKP)) {
return b.akp((PublicKey) key.getPublicKey());
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ org.keycloak.keys.GeneratedEcdhKeyProviderFactory
org.keycloak.keys.GeneratedEcdsaKeyProviderFactory
org.keycloak.keys.GeneratedRsaEncKeyProviderFactory
org.keycloak.keys.ImportedRsaEncKeyProviderFactory
org.keycloak.keys.GeneratedEddsaKeyProviderFactory
org.keycloak.keys.GeneratedEddsaKeyProviderFactory
org.keycloak.keys.GeneratedMlDsaKeyProviderFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.keycloak.tests.common;

import org.keycloak.common.Profile;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;

public final class SeverWithOAuthMlDsaEnabled implements KeycloakServerConfig {

@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.OAUTH_ML_DSA);
}
}
Loading
Loading
0