Allow to create EC certificates if new EC-key-provider is created (#31843)

Closes #31842

Signed-off-by: Pascal Knüppel <pascal.knueppel@governikus.de>
This commit is contained in:
Pascal Knüppel 2024-10-17 16:05:59 +02:00 committed by GitHub
parent 637ca2e138
commit 41ee68611f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 195 additions and 35 deletions

View file

@ -53,7 +53,7 @@ public abstract class AbstractJWKBuilder {
public JWK rsa(Key key) {
return rsa(key, null, KeyUse.SIG);
}
public JWK rsa(Key key, X509Certificate certificate) {
return rsa(key, Collections.singletonList(certificate), KeyUse.SIG);
}
@ -99,6 +99,10 @@ public abstract class AbstractJWKBuilder {
}
public JWK ec(Key key, KeyUse keyUse) {
return this.ec(key, null, keyUse);
}
public JWK ec(Key key, List<X509Certificate> certificates, KeyUse keyUse) {
ECPublicKey ecKey = (ECPublicKey) key;
ECPublicJWK k = new ECPublicJWK();
@ -113,7 +117,15 @@ public abstract class AbstractJWKBuilder {
k.setCrv("P-" + fieldSize);
k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize)));
k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize)));
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;
}

View file

@ -18,6 +18,9 @@
package org.keycloak.jose.jwk;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.common.util.PemUtils;
import java.security.NoSuchAlgorithmException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>

View file

@ -20,7 +20,9 @@ package org.keycloak.jose.jwk;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.common.util.PemUtils;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
@ -37,6 +39,12 @@ public class JWK {
public static final String PUBLIC_KEY_USE = "use";
public static final String X5C = "x5c";
public static final String SHA1_509_THUMBPRINT = "x5t";
public static final String SHA256_509_THUMBPRINT = "x5t#S256";
public enum Use {
SIG("sig"),
ENCRYPTION("enc");
@ -64,6 +72,13 @@ public class JWK {
@JsonProperty(PUBLIC_KEY_USE)
private String publicKeyUse;
@JsonProperty(X5C)
private String[] x509CertificateChain;
private String sha1x509Thumbprint;
private String sha256x509Thumbprint;
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
@ -99,6 +114,32 @@ public class JWK {
this.publicKeyUse = publicKeyUse;
}
public String[] getX509CertificateChain() {
return x509CertificateChain;
}
public void setX509CertificateChain(String[] x509CertificateChain) {
this.x509CertificateChain = x509CertificateChain;
if (x509CertificateChain != null && x509CertificateChain.length > 0) {
try {
sha1x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-1");
sha256x509Thumbprint = PemUtils.generateThumbprint(x509CertificateChain, "SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
@JsonProperty(SHA1_509_THUMBPRINT)
public String getSha1x509Thumbprint() {
return sha1x509Thumbprint;
}
@JsonProperty(SHA256_509_THUMBPRINT)
public String getSha256x509Thumbprint() {
return sha256x509Thumbprint;
}
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;

View file

@ -144,6 +144,7 @@ describe("Realm settings events tab tests", () => {
cy.findByTestId("option-ecdsa-generated").click();
realmSettingsPage.enterUIDisplayName("test_ecdsa-generated");
realmSettingsPage.toggleSwitch("active", false);
realmSettingsPage.toggleSwitch("ecGenerateCertificate", false);
realmSettingsPage.addProvider();
realmSettingsPage.toggleAddProviderDropdown();

View file

@ -194,21 +194,8 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
{
name: "publicKeys",
displayKey: "publicKeys",
cellRenderer: ({ type, publicKey, certificate }: KeyData) => {
if (type === "EC") {
return (
<Button
onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-public-key"
>
{t("publicKey")}
</Button>
);
} else if (type === "RSA") {
cellRenderer: ({ publicKey, certificate }: KeyData) => {
if (certificate) {
return (
<div className="button-wrapper">
<Button
@ -234,7 +221,7 @@ export const KeysListTab = ({ realmComponents }: KeysListTabProps) => {
</Button>
</div>
);
} else if (type === "OKP") {
} else if (publicKey) {
return (
<Button
onClick={() => {

View file

@ -25,6 +25,8 @@ import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.RealmModel;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.stream.Stream;
public abstract class AbstractEcKeyProvider implements KeyProvider {
@ -54,7 +56,8 @@ public abstract class AbstractEcKeyProvider implements KeyProvider {
return Stream.of(key);
}
protected KeyWrapper createKeyWrapper(KeyPair keyPair, String algorithm, KeyUse keyUse) {
protected KeyWrapper createKeyWrapper(KeyPair keyPair, String algorithm, KeyUse keyUse,
X509Certificate selfSignedCertificate) {
KeyWrapper key = new KeyWrapper();
key.setProviderId(model.getId());
@ -67,6 +70,7 @@ public abstract class AbstractEcKeyProvider implements KeyProvider {
key.setStatus(status);
key.setPrivateKey(keyPair.getPrivate());
key.setPublicKey(keyPair.getPublic());
Optional.ofNullable(selfSignedCertificate).ifPresent(key::setCertificate);
return key;
}

View file

@ -36,7 +36,8 @@ public abstract class AbstractEcKeyProviderFactory<T extends KeyProvider> implem
return ProviderConfigurationBuilder.create()
.property(Attributes.PRIORITY_PROPERTY)
.property(Attributes.ENABLED_PROPERTY)
.property(Attributes.ACTIVE_PROPERTY);
.property(Attributes.ACTIVE_PROPERTY)
.property(Attributes.EC_GENERATE_CERTIFICATE_PROPERTY);
}
@Override
@ -44,7 +45,8 @@ public abstract class AbstractEcKeyProviderFactory<T extends KeyProvider> implem
ConfigurationValidationHelper.check(model)
.checkLong(Attributes.PRIORITY_PROPERTY, false)
.checkBoolean(Attributes.ENABLED_PROPERTY, false)
.checkBoolean(Attributes.ACTIVE_PROPERTY, false);
.checkBoolean(Attributes.ACTIVE_PROPERTY, false)
.checkBoolean(Attributes.EC_GENERATE_CERTIFICATE_PROPERTY, false);
}
public static KeyPair generateEcKeyPair(String keySpecName) {

View file

@ -55,6 +55,17 @@ public interface Attributes {
ProviderConfigProperty KEY_USE_PROPERTY = new ProviderConfigProperty(KEY_USE, "Key use", "Whether the key should be used for signing or encryption.", LIST_TYPE,
KeyUse.SIG.getSpecName(), KeyUse.SIG.getSpecName(), KeyUse.ENC.getSpecName());
String EC_GENERATE_CERTIFICATE_KEY = "ecGenerateCertificate";
ProviderConfigProperty EC_GENERATE_CERTIFICATE_PROPERTY = new ProviderConfigProperty(
EC_GENERATE_CERTIFICATE_KEY,
"Generate Certificate",
"""
If a certificate should be build on creation. If the certificate is build, it will be available in the \
realm JWK for the key in the claim x5c and corresponding thumbprints may be available in the claims like \
x5t or x5t#S256.""",
BOOLEAN_TYPE,
false);
String KID_KEY = "kid";
String SECRET_KEY = "secret";

View file

@ -18,6 +18,8 @@ package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
@ -27,8 +29,11 @@ import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.Optional;
public class GeneratedEcdhKeyProvider extends AbstractEcKeyProvider {
private static final Logger logger = Logger.getLogger(GeneratedEcdhKeyProvider.class);
@ -42,6 +47,10 @@ public class GeneratedEcdhKeyProvider extends AbstractEcKeyProvider {
String privateEcdhKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdhKeyProviderFactory.ECDH_PRIVATE_KEY_KEY);
String publicEcdhKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdhKeyProviderFactory.ECDH_PUBLIC_KEY_KEY);
String ecdhAlgorithm = model.getConfig().getFirst(GeneratedEcdhKeyProviderFactory.ECDH_ALGORITHM_KEY);
boolean generateCertificate = Optional.ofNullable(model.getConfig()
.getFirst(Attributes.EC_GENERATE_CERTIFICATE_KEY))
.map(Boolean::parseBoolean)
.orElse(false);
try {
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEcdhKeyBase64Encoded));
@ -52,9 +61,20 @@ public class GeneratedEcdhKeyProvider extends AbstractEcKeyProvider {
PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec);
KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey);
X509Certificate selfSignedCertificate = Optional.ofNullable(model.getConfig()
.getFirst(Attributes.CERTIFICATE_KEY))
.map(PemUtils::decodeCertificate)
.orElse(null);
if (generateCertificate && selfSignedCertificate == null)
{
selfSignedCertificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName());
model.getConfig().put(Attributes.CERTIFICATE_KEY,
List.of(Base64.encodeBytes(selfSignedCertificate.getEncoded())));
}
return createKeyWrapper(keyPair, ecdhAlgorithm, KeyUse.ENC);
return createKeyWrapper(keyPair, ecdhAlgorithm, KeyUse.ENC, selfSignedCertificate);
} catch (Exception e) {
logger.debug(e.getMessage(), e);
logger.warnf("Exception at decodeEcdhPublicKey. %s", e.toString());
return null;
}

View file

@ -18,17 +18,24 @@ package org.keycloak.keys;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.RealmModel;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public class GeneratedEcdsaKeyProvider extends AbstractEcKeyProvider {
private static final Logger logger = Logger.getLogger(GeneratedEcdsaKeyProvider.class);
@ -42,6 +49,10 @@ public class GeneratedEcdsaKeyProvider extends AbstractEcKeyProvider {
String privateEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PRIVATE_KEY_KEY);
String publicEcdsaKeyBase64Encoded = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_PUBLIC_KEY_KEY);
String ecInNistRep = model.getConfig().getFirst(GeneratedEcdsaKeyProviderFactory.ECDSA_ELLIPTIC_CURVE_KEY);
boolean generateCertificate = Optional.ofNullable(model.getConfig()
.getFirst(Attributes.EC_GENERATE_CERTIFICATE_KEY))
.map(Boolean::parseBoolean)
.orElse(false);
try {
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateEcdsaKeyBase64Encoded));
@ -52,9 +63,23 @@ public class GeneratedEcdsaKeyProvider extends AbstractEcKeyProvider {
PublicKey decodedPublicKey = kf.generatePublic(publicKeySpec);
KeyPair keyPair = new KeyPair(decodedPublicKey, decodedPrivateKey);
X509Certificate selfSignedCertificate = Optional.ofNullable(model.getConfig()
.getFirst(Attributes.CERTIFICATE_KEY))
.map(PemUtils::decodeCertificate)
.orElse(null);
if (generateCertificate && selfSignedCertificate == null)
{
selfSignedCertificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName());
model.getConfig().put(Attributes.CERTIFICATE_KEY,
List.of(Base64.encodeBytes(selfSignedCertificate.getEncoded())));
}
return createKeyWrapper(keyPair,
GeneratedEcdsaKeyProviderFactory.convertECDomainParmNistRepToJWSAlgorithm(ecInNistRep), KeyUse.SIG);
GeneratedEcdsaKeyProviderFactory.convertECDomainParmNistRepToJWSAlgorithm(
ecInNistRep), KeyUse.SIG,
selfSignedCertificate);
} catch (Exception e) {
logger.debug(e.getMessage(), e);
logger.warnf("Exception at decodeEcdsaPublicKey. %s", e.toString());
return null;
}

View file

@ -22,7 +22,6 @@ import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.wellknown.WellKnownProvider;
import java.security.cert.X509Certificate;
import java.util.Collections;
@ -41,11 +40,12 @@ import java.util.Optional;
JWKBuilder b = JWKBuilder.create().kid(k.getKid()).algorithm(k.getAlgorithmOrDefault());
List<X509Certificate> certificates = Optional.ofNullable(k.getCertificateChain())
.filter(certs -> !certs.isEmpty())
.orElseGet(() -> Collections.singletonList(k.getCertificate()));
.orElseGet(() -> Optional.ofNullable(k.getCertificate()).map(Collections::singletonList)
.orElseGet(Collections::emptyList));
if (k.getType().equals(KeyType.RSA)) {
return b.rsa(k.getPublicKey(), certificates, k.getUse());
} else if (k.getType().equals(KeyType.EC)) {
return b.ec(k.getPublicKey(), k.getUse());
return b.ec(k.getPublicKey(), certificates, k.getUse());
} else if (k.getType().equals(KeyType.OKP)) {
return b.okp(k.getPublicKey(), k.getUse());
}

View file

@ -21,6 +21,7 @@ import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.models.KeycloakSession;
@ -33,6 +34,7 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
@ -90,6 +92,21 @@ public class KeyResource {
r.setType(key.getType());
r.setAlgorithm(key.getAlgorithmOrDefault());
r.setPublicKey(key.getPublicKey() != null ? PemUtils.encodeKey(key.getPublicKey()) : null);
if (key.getCertificate() != null ||
(key.getCertificateChain() != null && !key.getCertificateChain().isEmpty())) {
try {
final String base64Certificate;
if (key.getCertificate() != null) {
base64Certificate = Base64.encodeBytes(key.getCertificate().getEncoded());
}
else {
base64Certificate = Base64.encodeBytes(key.getCertificateChain().get(0).getEncoded());
}
r.setCertificate(base64Certificate);
} catch (CertificateEncodingException e) {
throw new RuntimeException(e);
}
}
r.setUse(key.getUse());
X509Certificate cert = key.getCertificate();

View file

@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import java.security.KeyFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
@ -34,6 +35,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.KeyType;
import org.keycloak.keys.Attributes;
import org.keycloak.keys.GeneratedEcdsaKeyProviderFactory;
@ -64,28 +66,49 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"),
RealmRepresentation.class);
testRealms.add(realm);
}
@Test
public void defaultEc() {
supportedEc(null);
supportedEc(null, true);
}
@Test
public void defaultEcWithCertificate() {
supportedEc(null, false);
}
@Test
public void supportedEcP521() {
supportedEc("P-521");
supportedEc("P-521", false);
}
@Test
public void supportedEcP521WithCertificate() {
supportedEc("P-521", true);
}
@Test
public void supportedEcP384() {
supportedEc("P-384");
supportedEc("P-384", false);
}
@Test
public void supportedEcP384WithCertificate() {
supportedEc("P-384", true);
}
@Test
public void supportedEcP256() {
supportedEc("P-256");
supportedEc("P-256", false);
}
@Test
public void supportedEcP256AndCertificate() {
supportedEc("P-256", true);
}
@Test
@ -94,7 +117,7 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
unsupportedEc("K-163");
}
private String supportedEc(String ecInNistRep) {
private String supportedEc(String ecInNistRep, boolean withCertificate) {
long priority = System.currentTimeMillis();
ComponentRepresentation rep = createRep("valid", GeneratedEcdsaKeyProviderFactory.ID);
@ -102,9 +125,13 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle(Attributes.PRIORITY_KEY, Long.toString(priority));
if (ecInNistRep != null) {
rep.getConfig().putSingle(ECDSA_ELLIPTIC_CURVE_KEY, ecInNistRep);
} else {
}
else {
ecInNistRep = DEFAULT_EC;
}
if (withCertificate) {
rep.getConfig().putSingle(Attributes.EC_GENERATE_CERTIFICATE_KEY, "true");
}
Response response = adminClient.realm(TEST_REALM_NAME).components().add(rep);
String id = ApiUtil.getCreatedId(response);
@ -114,9 +141,12 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
ComponentRepresentation createdRep = adminClient.realm(TEST_REALM_NAME).components().component(id).toRepresentation();
// stands for the number of properties in the key provider config
assertEquals(2, createdRep.getConfig().size());
assertEquals(withCertificate ? 3 : 2, createdRep.getConfig().size());
assertEquals(Long.toString(priority), createdRep.getConfig().getFirst(Attributes.PRIORITY_KEY));
assertEquals(ecInNistRep, createdRep.getConfig().getFirst(ECDSA_ELLIPTIC_CURVE_KEY));
if (withCertificate) {
assertNotNull(createdRep.getConfig().getFirst(Attributes.EC_GENERATE_CERTIFICATE_KEY));
}
KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata();
@ -133,6 +163,13 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
assertEquals(id, key.getProviderId());
assertEquals(KeyType.EC, key.getType());
assertEquals(priority, key.getProviderPriority());
if (withCertificate) {
assertNotNull(key.getCertificate());
X509Certificate certificate = PemUtils.decodeCertificate(key.getCertificate());
final String expectedIssuerAndSubject = "CN=" + TEST_REALM_NAME;
assertEquals(expectedIssuerAndSubject, certificate.getIssuerX500Principal().getName());
assertEquals(expectedIssuerAndSubject, certificate.getSubjectX500Principal().getName());
}
return id; // created key's component id
}
@ -176,7 +213,7 @@ public class GeneratedEcdsaKeyProviderTest extends AbstractKeycloakTest {
}
private void changeCurve(String FromEcInNistRep, String ToEcInNistRep) throws Exception {
String keyComponentId = supportedEc(FromEcInNistRep);
String keyComponentId = supportedEc(FromEcInNistRep, false);
KeysMetadataRepresentation keys = adminClient.realm(TEST_REALM_NAME).keys().getKeyMetadata();
KeysMetadataRepresentation.KeyMetadataRepresentation originalKey = null;
for (KeyMetadataRepresentation k : keys.getKeys()) {