Support EC Key-Imports for the JavaKeystoreKeyProvider #26936 (#27030)

closes #26936

Signed-off-by: Stefan Wiedemann <wistefan@googlemail.com>
This commit is contained in:
Stefan Wiedemann 2024-02-19 17:41:40 +01:00 committed by GitHub
parent 018914d7fd
commit aa6b102e3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 546 additions and 117 deletions

View file

@ -1,13 +1,14 @@
package org.keycloak.common.crypto;
import java.io.IOException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
public interface ECDSACryptoProvider {
public byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException;
public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException;
public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey);
}

View file

@ -67,7 +67,7 @@ import java.util.LinkedList;
import java.util.List;
/**
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link X509Certificate}
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:giriraj.sharma27@gmail.com">Giriraj Sharma</a>
@ -76,15 +76,13 @@ import java.util.List;
public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
/**
* Generates version 3 {@link java.security.cert.X509Certificate}.
* Generates version 3 {@link X509Certificate}.
*
* @param keyPair the key pair
* @param caPrivateKey the CA private key
* @param caCert the CA certificate
* @param subject the subject name
*
* @return the x509 certificate
*
* @throws Exception the exception
*/
public X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert,
@ -141,13 +139,11 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
}
/**
* Generate version 1 self signed {@link java.security.cert.X509Certificate}..
* Generate version 1 self signed {@link X509Certificate}..
*
* @param caKeyPair the CA key pair
* @param subject the subject name
*
* @return the x509 certificate
*
* @throws Exception the exception
*/
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject) {
@ -174,16 +170,30 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
}
/**
* Creates the content signer for generation of Version 1 {@link java.security.cert.X509Certificate}.
* Creates the content signer for generation of Version 1 {@link X509Certificate}.
*
* @param privateKey the private key
*
* @return the content signer
*/
private ContentSigner createSigner(PrivateKey privateKey) {
try {
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
JcaContentSignerBuilder signerBuilder;
switch (privateKey.getAlgorithm()) {
case "RSA": {
signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
.setProvider(BouncyIntegration.PROVIDER);
break;
}
case "ECDSA": {
signerBuilder = new JcaContentSignerBuilder("SHA256WithECDSA")
.setProvider(BouncyIntegration.PROVIDER);
break;
}
default: {
throw new RuntimeException(String.format("Keytype %s is not supported.", privateKey.getAlgorithm()));
}
}
return signerBuilder.build(privateKey);
} catch (Exception e) {
throw new RuntimeException("Could not create content signer.", e);
@ -212,6 +222,7 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
/**
* Retrieves a list of CRL distribution points from CRLDP v3 certificate extension
* See <a href="www.nakov.com/blog/2009/12/01/x509-certificate-validation-in-java-build-and-verify-cchain-and-verify-clr-with-bouncy-castle/">CRL validation</a>
*
* @param cert
* @return
* @throws IOException
@ -225,7 +236,7 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
List<String> distributionPointUrls = new LinkedList<>();
DEROctetString octetString;
try (ASN1InputStream crldpExtensionInputStream = new ASN1InputStream(new ByteArrayInputStream(data))) {
octetString = (DEROctetString)crldpExtensionInputStream.readObject();
octetString = (DEROctetString) crldpExtensionInputStream.readObject();
}
byte[] octets = octetString.getOctets();
@ -268,11 +279,9 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate,
subjectDN, subjPubKeyInfo);
if (certificatePolicyOid != null)
{
try
{
for (Extension certExtension: certPolicyExtensions(certificatePolicyOid))
if (certificatePolicyOid != null) {
try {
for (Extension certExtension : certPolicyExtensions(certificatePolicyOid))
certGen.addExtension(certExtension);
} catch (CertIOException e) {
throw new IllegalStateException(e);
@ -297,11 +306,9 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
private List<Extension> certPolicyExtensions(String... certificatePolicyOid) {
List<Extension> certificatePolicies = new LinkedList<>();
if (certificatePolicyOid != null && certificatePolicyOid.length > 0)
{
if (certificatePolicyOid != null && certificatePolicyOid.length > 0) {
List<PolicyInformation> policyInfoList = new LinkedList<>();
for (String oid: certificatePolicyOid)
{
for (String oid : certificatePolicyOid) {
policyInfoList.add(new PolicyInformation(new ASN1ObjectIdentifier(oid)));
}

View file

@ -1,17 +1,26 @@
package org.keycloak.crypto.def;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequenceGenerator;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.math.ec.ECPoint;
import org.keycloak.common.crypto.ECDSACryptoProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
public class BCECDSACryptoProvider implements ECDSACryptoProvider {
@ -60,5 +69,22 @@ public class BCECDSACryptoProvider implements ECDSACryptoProvider {
return concatenatedSignatureValue;
}
@Override
public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) {
try {
BCECPrivateKey bcecPrivateKey = new BCECPrivateKey(ecPrivateKey, BouncyCastleProvider.CONFIGURATION);
ECPoint q = bcecPrivateKey.getParameters().getG().multiply(bcecPrivateKey.getD());
ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(q, bcecPrivateKey.getParameters());
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return (ECPublicKey) keyFactory.generatePublic(publicKeySpec);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Key algorithm not supported.", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Received an invalid key spec.", e);
}
}
}

View file

@ -0,0 +1,81 @@
/*
* 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.def.test;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.crypto.def.BCECDSACryptoProvider;
import org.keycloak.rule.CryptoInitRule;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class BCECDSACryptoProviderTest {
@ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{"secp256r1"}, {"secp384r1"}, {"secp521r1"}
});
}
private String curve;
public BCECDSACryptoProviderTest(String curve) {
this.curve = curve;
}
@Test
public void getPublicFromPrivate() {
KeyPair testKey = generateECKey(curve);
BCECDSACryptoProvider bcecdsaCryptoProvider = new BCECDSACryptoProvider();
assertEquals("The derived key should be equal to the originally generated one.",
testKey.getPublic(),
bcecdsaCryptoProvider.getPublicFromPrivate((ECPrivateKey) testKey.getPrivate()));
}
public static KeyPair generateECKey(String curve) {
try {
KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA");
ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve);
kpg.initialize(parameterSpec);
return kpg.generateKeyPair();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -18,6 +18,8 @@ package org.keycloak.crypto.elytron;
import java.io.IOException;
import java.math.BigInteger;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import org.jboss.logging.Logger;
import org.keycloak.common.crypto.ECDSACryptoProvider;
@ -60,8 +62,8 @@ public class ElytronECDSACryptoProvider implements ECDSACryptoProvider {
DERDecoder der = new DERDecoder(derEncodedSignatureValue);
der.startSequence();
byte[] r = convertToBytes(der.decodeInteger(),len);
byte[] s = convertToBytes(der.decodeInteger(),len);
byte[] r = convertToBytes(der.decodeInteger(), len);
byte[] s = convertToBytes(der.decodeInteger(), len);
der.endSequence();
byte[] concatenatedSignatureValue = new byte[signLength];
@ -71,13 +73,18 @@ public class ElytronECDSACryptoProvider implements ECDSACryptoProvider {
return concatenatedSignatureValue;
}
@Override
public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) {
throw new UnsupportedOperationException("Elytron Crypto Provider currently does not support extraction of EC Public Keys.");
}
// If byte array length doesn't match expected length, copy to new
// byte array of the expected length
private byte[] convertToBytes(BigInteger decodeInteger, int len) {
byte[] bytes = decodeInteger.toByteArray();
if(len < bytes.length) {
if (len < bytes.length) {
log.debug("Decoded integer byte length greater than expected.");
byte[] t = new byte[len];
System.arraycopy(bytes, bytes.length - len, t, 0, len);

View file

@ -67,7 +67,7 @@ import java.util.LinkedList;
import java.util.List;
/**
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link X509Certificate}
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:giriraj.sharma27@gmail.com">Giriraj Sharma</a>
@ -76,7 +76,7 @@ import java.util.List;
public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{
/**
* Generates version 3 {@link java.security.cert.X509Certificate}.
* Generates version 3 {@link X509Certificate}.
*
* @param keyPair the key pair
* @param caPrivateKey the CA private key
@ -141,7 +141,7 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{
}
/**
* Generate version 1 self signed {@link java.security.cert.X509Certificate}..
* Generate version 1 self signed {@link X509Certificate}..
*
* @param caKeyPair the CA key pair
* @param subject the subject name
@ -174,7 +174,7 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{
}
/**
* Creates the content signer for generation of Version 1 {@link java.security.cert.X509Certificate}.
* Creates the content signer for generation of Version 1 {@link X509Certificate}.
*
* @param privateKey the private key
*
@ -182,8 +182,23 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{
*/
private ContentSigner createSigner(PrivateKey privateKey) {
try {
JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
JcaContentSignerBuilder signerBuilder;
switch (privateKey.getAlgorithm()) {
case "RSA": {
signerBuilder = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
.setProvider(BouncyIntegration.PROVIDER);
break;
}
case "EC": {
signerBuilder = new JcaContentSignerBuilder("SHA256WithECDSA")
.setProvider(BouncyIntegration.PROVIDER);
break;
}
default: {
throw new RuntimeException(String.format("Keytype %s is not supported.", privateKey.getAlgorithm()));
}
}
return signerBuilder.build(privateKey);
} catch (Exception e) {
throw new RuntimeException("Could not create content signer.", e);

View file

@ -1,17 +1,26 @@
package org.keycloak.crypto.fips;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequenceGenerator;
import org.bouncycastle.asn1.x9.X9IntegerConverter;
import org.bouncycastle.jcajce.spec.ECDomainParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
import org.keycloak.common.crypto.ECDSACryptoProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
public class BCFIPSECDSACryptoProvider implements ECDSACryptoProvider {
@ -60,5 +69,27 @@ public class BCFIPSECDSACryptoProvider implements ECDSACryptoProvider {
return concatenatedSignatureValue;
}
@Override
public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) {
try {
ECParameterSpec parameterSpec = ecPrivateKey.getParams();
ECDomainParameterSpec domainParameterSpec = new ECDomainParameterSpec(parameterSpec);
ECPoint q = domainParameterSpec.getDomainParameters().getG().multiply(ecPrivateKey.getS()).normalize();
ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(
new java.security.spec.ECPoint(
q.getAffineXCoord().toBigInteger(),
q.getAffineYCoord().toBigInteger()),
domainParameterSpec);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Key algorithm not supported.", e);
} catch (InvalidKeySpecException e) {
throw new RuntimeException("Received an invalid key spec.", e);
}
}
}

View file

@ -0,0 +1,85 @@
/*
* 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.fips.test;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.fips.BCFIPSECDSACryptoProvider;
import org.keycloak.keys.AbstractEcdsaKeyProviderFactory;
import org.keycloak.rule.CryptoInitRule;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class BCFIPSECDSACryptoProviderTest {
@ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{Algorithm.ES256}, {Algorithm.ES384}, {Algorithm.ES512}
});
}
private String algorithm;
public BCFIPSECDSACryptoProviderTest(String algorithm) {
this.algorithm = algorithm;
}
@Test
public void getPublicFromPrivate() {
KeyPair testKey = generateECKey(algorithm);
BCFIPSECDSACryptoProvider bcfipsecdsaCryptoProvider = new BCFIPSECDSACryptoProvider();
ECPublicKey derivedKey = bcfipsecdsaCryptoProvider.getPublicFromPrivate((ECPrivateKey) testKey.getPrivate());
assertEquals("The derived key should be equal to the originally generated one.",
testKey.getPublic(),
derivedKey);
}
public static KeyPair generateECKey(String algorithm) {
try {
KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA");
String domainParamNistRep = AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm);
String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep);
ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve);
kpg.initialize(parameterSpec);
return kpg.generateKeyPair();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -17,12 +17,17 @@
package org.keycloak.keys;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.component.ComponentModel;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.models.RealmModel;
import java.io.FileInputStream;
@ -43,6 +48,7 @@ import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPrivateKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@ -50,39 +56,49 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider {
public class JavaKeystoreKeyProvider implements KeyProvider {
private final KeyStatus status;
private final ComponentModel model;
private final KeyWrapper key;
private final String algorithm;
public JavaKeystoreKeyProvider(RealmModel realm, ComponentModel model) {
super(realm, model);
this.model = model;
this.status = KeyStatus.from(model.get(Attributes.ACTIVE_KEY, true), model.get(Attributes.ENABLED_KEY, true));
String defaultAlgorithmKey = KeyUse.ENC.name().equals(model.get(Attributes.KEY_USE)) ? JWEConstants.RSA_OAEP : Algorithm.RS256;
this.algorithm = model.get(Attributes.ALGORITHM_KEY, defaultAlgorithmKey);
if (model.hasNote(KeyWrapper.class.getName())) {
key = model.getNote(KeyWrapper.class.getName());
} else {
key = loadKey(realm, model);
model.setNote(KeyWrapper.class.getName(), key);
}
}
@Override
protected KeyWrapper loadKey(RealmModel realm, ComponentModel model) {
String keystorePath = model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_KEY);
try (FileInputStream is = new FileInputStream(keystorePath)) {
// Use "JKS" as default type for backwards compatibility
String keystoreType = KeystoreUtil.getKeystoreType(model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_TYPE_KEY), keystorePath, "JKS");
KeyStore keyStore = KeyStore.getInstance(keystoreType);
keyStore.load(is, model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_PASSWORD_KEY).toCharArray());
KeyStore keyStore = loadKeyStore(is, keystorePath);
String keyAlias = model.get(JavaKeystoreKeyProviderFactory.KEY_ALIAS_KEY);
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, model.get(JavaKeystoreKeyProviderFactory.KEY_PASSWORD_KEY).toCharArray());
PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
KeyPair keyPair = new KeyPair(publicKey, privateKey);
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyAlias);
if (certificate == null) {
certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realm.getName());
}
KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.getSpecName()).toUpperCase());
return createKeyWrapper(keyPair, certificate, loadCertificateChain(keyStore, keyAlias), keyUse);
return switch (algorithm) {
case Algorithm.PS256, Algorithm.PS384, Algorithm.PS512, Algorithm.RS256, Algorithm.RS384, Algorithm.RS512 ->
loadRSAKey(realm, model, keyStore, keyAlias);
case Algorithm.ES256, Algorithm.ES384, Algorithm.ES512 -> loadECKey(realm, model, keyStore, keyAlias);
default ->
throw new RuntimeException(String.format("Keys for algorithm %s are not supported.", algorithm));
};
} catch (KeyStoreException kse) {
throw new RuntimeException("KeyStore error on server. " + kse.getMessage(), kse);
} catch (FileNotFoundException fnfe) {
@ -100,6 +116,44 @@ public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider {
}
}
private KeyStore loadKeyStore(FileInputStream inputStream, String keystorePath) throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
// Use "JKS" as default type for backwards compatibility
String keystoreType = KeystoreUtil.getKeystoreType(model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_TYPE_KEY), keystorePath, "JKS");
KeyStore keyStore = KeyStore.getInstance(keystoreType);
keyStore.load(inputStream, model.get(JavaKeystoreKeyProviderFactory.KEYSTORE_PASSWORD_KEY).toCharArray());
return keyStore;
}
private KeyWrapper loadECKey(RealmModel realm, ComponentModel model, KeyStore keyStore, String keyAlias) throws GeneralSecurityException {
ECPrivateKey privateKey = (ECPrivateKey) keyStore.getKey(keyAlias, model.get(JavaKeystoreKeyProviderFactory.KEY_PASSWORD_KEY).toCharArray());
String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm));
PublicKey publicKey = CryptoIntegration.getProvider().getEcdsaCryptoProvider().getPublicFromPrivate(privateKey);
KeyPair keyPair = new KeyPair(publicKey, privateKey);
return createKeyWrapper(keyPair, getCertificate(keyStore, keyPair, keyAlias, realm.getName()), loadCertificateChain(keyStore, keyAlias), KeyType.EC);
}
private X509Certificate getCertificate(KeyStore keyStore, KeyPair keyPair, String keyAlias, String realmName) throws KeyStoreException {
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyAlias);
if (certificate == null) {
certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, realmName);
}
return certificate;
}
private KeyWrapper loadRSAKey(RealmModel realm, ComponentModel model, KeyStore keyStore, String keyAlias) throws GeneralSecurityException {
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, model.get(JavaKeystoreKeyProviderFactory.KEY_PASSWORD_KEY).toCharArray());
PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
KeyPair keyPair = new KeyPair(publicKey, privateKey);
return createKeyWrapper(keyPair, getCertificate(keyStore, keyPair, keyAlias, realm.getName()), loadCertificateChain(keyStore, keyAlias), KeyType.RSA);
}
private List<X509Certificate> loadCertificateChain(KeyStore keyStore, String keyAlias) throws GeneralSecurityException {
List<X509Certificate> chain = Optional.ofNullable(keyStore.getCertificateChain(keyAlias))
.map(certificates -> Arrays.stream(certificates)
@ -112,6 +166,34 @@ public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider {
return chain;
}
private KeyWrapper createKeyWrapper(KeyPair keyPair, X509Certificate certificate, List<X509Certificate> certificateChain, String type) {
KeyUse keyUse = KeyUse.valueOf(model.get(Attributes.KEY_USE, KeyUse.SIG.getSpecName()).toUpperCase());
KeyWrapper key = new KeyWrapper();
key.setProviderId(model.getId());
key.setProviderPriority(model.get("priority", 0L));
key.setKid(model.get(Attributes.KID_KEY) != null ? model.get(Attributes.KID_KEY) : KeyUtils.createKeyId(keyPair.getPublic()));
key.setUse(keyUse);
key.setType(type);
key.setAlgorithm(algorithm);
key.setStatus(status);
key.setPrivateKey(keyPair.getPrivate());
key.setPublicKey(keyPair.getPublic());
key.setCertificate(certificate);
if (!certificateChain.isEmpty()) {
if (certificate != null && !certificate.equals(certificateChain.get(0))) {
// just in case the chain does not contain the end-user certificate
certificateChain.add(0, certificate);
}
key.setCertificateChain(certificateChain);
}
return key;
}
/**
* <p>Validates the giving certificate chain represented by {@code certificates}. If the list of certificates is empty
* or does not have at least 2 certificates (end-user certificate plus intermediary/root CAs) this method does nothing.
@ -140,4 +222,10 @@ public class JavaKeystoreKeyProvider extends AbstractRsaKeyProvider {
validator.validate(certPath, params);
}
@Override
public Stream<KeyWrapper> getKeysStream() {
return Stream.of(key);
}
}

View file

@ -23,12 +23,15 @@ import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
import java.util.stream.Stream;
import static org.keycloak.provider.ProviderConfigProperty.LIST_TYPE;
import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
@ -36,7 +39,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactory {
public class JavaKeystoreKeyProviderFactory implements KeyProviderFactory {
private static final Logger logger = Logger.getLogger(JavaKeystoreKeyProviderFactory.class);
public static final String ID = "java-keystore";
@ -62,6 +65,7 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
private List<ProviderConfigProperty> configProperties;
@Override
public void init(Config.Scope config) {
String[] supportedKeystoreTypes = CryptoIntegration.getProvider().getSupportedKeyStoreTypes()
@ -71,7 +75,11 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
"Keystore type. This parameter is not mandatory. If omitted, the type will be detected from keystore file or default keystore type will be used", LIST_TYPE,
supportedKeystoreTypes.length > 0 ? supportedKeystoreTypes[0] : null, supportedKeystoreTypes);
configProperties = AbstractRsaKeyProviderFactory.configurationBuilder()
configProperties = ProviderConfigurationBuilder.create()
.property(Attributes.PRIORITY_PROPERTY)
.property(Attributes.ENABLED_PROPERTY)
.property(Attributes.ACTIVE_PROPERTY)
.property(mergedAlgorithmProperties())
.property(KEYSTORE_PROPERTY)
.property(KEYSTORE_PASSWORD_PROPERTY)
.property(keystoreTypeProperty)
@ -88,9 +96,11 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
super.validateConfiguration(session, realm, model);
ConfigurationValidationHelper.check(model)
.checkLong(Attributes.PRIORITY_PROPERTY, false)
.checkBoolean(Attributes.ENABLED_PROPERTY, false)
.checkBoolean(Attributes.ACTIVE_PROPERTY, false)
.checkSingle(KEYSTORE_PROPERTY, true)
.checkSingle(KEYSTORE_PASSWORD_PROPERTY, true)
.checkSingle(keystoreTypeProperty, false)
@ -105,6 +115,14 @@ public class JavaKeystoreKeyProviderFactory extends AbstractRsaKeyProviderFactor
}
}
// merge the algorithms supported for RSA and EC keys and provide them as one configuration property
private static ProviderConfigProperty mergedAlgorithmProperties() {
List<String> ecAlgorithms = List.of(Algorithm.ES256, Algorithm.ES384, Algorithm.ES512);
List<String> algorithms = Stream.concat(Attributes.RS_ALGORITHM_PROPERTY.getOptions().stream(), ecAlgorithms.stream()).toList();
return new ProviderConfigProperty(Attributes.ALGORITHM_KEY, "Algorithm", "Intended algorithm for the key", LIST_TYPE, algorithms.toArray());
}
@Override
public String getHelpText() {
return HELP_TEXT;

View file

@ -1,22 +1,27 @@
package org.keycloak.testsuite.util;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.keys.AbstractEcdsaKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation;
import org.keycloak.testsuite.admin.ApiUtil;
import jakarta.ws.rs.core.Response;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
@ -31,6 +36,20 @@ import static org.junit.Assert.fail;
* @author mhajas
*/
public class KeyUtils {
public static KeyPair generateECKey(String algorithm) {
try {
KeyPairGenerator kpg = CryptoIntegration.getProvider().getKeyPairGen("ECDSA");
String domainParamNistRep = AbstractEcdsaKeyProviderFactory.convertAlgorithmToECDomainParmNistRep(algorithm);
String curve = AbstractEcdsaKeyProviderFactory.convertECDomainParmNistRepToSecRep(domainParamNistRep);
ECGenParameterSpec parameterSpec = new ECGenParameterSpec(curve);
kpg.initialize(parameterSpec);
return kpg.generateKeyPair();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
}
public static PublicKey publicKeyFromString(String key) {
try {
@ -106,6 +125,7 @@ public class KeyUtils {
public static AutoCloseable generateNewRealmKey(RealmResource realm, KeyUse keyUse, String algorithm) {
return generateNewRealmKey(realm, keyUse, algorithm, "100");
}
/**
* @return key sizes, which are expected to be supported by Keycloak server for {@link org.keycloak.keys.GeneratedRsaKeyProviderFactory} and {@link org.keycloak.keys.GeneratedRsaEncKeyProviderFactory}.
*/

View file

@ -19,15 +19,6 @@
package org.keycloak.testsuite.util;
import java.io.File;
import java.io.FileOutputStream;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.Assume;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.crypto.CryptoIntegration;
@ -37,6 +28,15 @@ import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.PemUtils;
import org.keycloak.representations.idm.CertificateRepresentation;
import java.io.File;
import java.io.FileOutputStream;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.stream.Stream;
import static org.junit.Assert.fail;
/**
@ -63,10 +63,9 @@ public class KeystoreUtils {
.anyMatch(type -> type.equals(keystoreType.toString())));
}
public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword) throws Exception {
public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword, KeyPair keyPair) throws Exception {
String fileName = "keystore." + keystoreType.getPrimaryExtension();
KeyPair keyPair = KeyUtils.generateRsaKeyPair(2048);
X509Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, subject);
KeyStore keyStore = CryptoIntegration.getProvider().getKeyStore(keystoreType);
@ -85,6 +84,10 @@ public class KeystoreUtils {
return new KeystoreInfo(certRep, file);
}
public static KeystoreInfo generateKeystore(TemporaryFolder folder, KeystoreUtil.KeystoreFormat keystoreType, String subject, String keystorePassword, String keyPassword) throws Exception {
return generateKeystore(folder, keystoreType, subject, keystorePassword, keyPassword, KeyUtils.generateRsaKeyPair(2048));
}
public static class KeystoreInfo {
private final CertificateRepresentation certificateInfo;
private final File keystoreFile;

View file

@ -17,12 +17,15 @@
package org.keycloak.testsuite.keys;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.PemUtils;
import org.keycloak.crypto.Algorithm;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.keys.JavaKeystoreKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
@ -36,13 +39,14 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.KeystoreUtils;
import jakarta.ws.rs.core.Response;
import java.security.PublicKey;
import java.util.List;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.common.util.KeystoreUtil.KeystoreFormat.PKCS12;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@ -64,6 +68,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
@Page
protected LoginPage loginPage;
private KeystoreUtils.KeystoreInfo generatedKeystore;
private String keyAlgorithm;
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
@ -72,34 +77,49 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
}
@Test
public void createJks() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.JKS);
public void createJksRSA() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.RSA);
}
@Test
public void createPkcs12() throws Exception {
createSuccess(PKCS12);
public void createPkcs12RSA() throws Exception {
createSuccess(PKCS12, AlgorithmType.RSA);
}
@Test
public void createBcfks() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.BCFKS);
public void createBcfksRSA() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.RSA);
}
private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType) throws Exception {
@Test
public void createJksECDSA() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.JKS, AlgorithmType.ECDSA);
}
@Test
public void createPkcs12ECDSA() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.PKCS12, AlgorithmType.ECDSA);
}
@Test
public void createBcfksECDSA() throws Exception {
createSuccess(KeystoreUtil.KeystoreFormat.BCFKS, AlgorithmType.ECDSA);
}
private void createSuccess(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception {
KeystoreUtils.assumeKeystoreTypeSupported(keystoreType);
generateKeystore(keystoreType);
generateKeystore(keystoreType, algorithmType);
long priority = System.currentTimeMillis();
ComponentRepresentation rep = createRep("valid", priority);
ComponentRepresentation rep = createRep("valid", priority, keyAlgorithm);
Response response = adminClient.realm("test").components().add(rep);
String id = ApiUtil.getCreatedId(response);
getCleanup().addComponentId(id);
ComponentRepresentation createdRep = adminClient.realm("test").components().component(id).toRepresentation();
assertEquals(5, createdRep.getConfig().size());
assertEquals(6, createdRep.getConfig().size());
assertEquals(Long.toString(priority), createdRep.getConfig().getFirst("priority"));
assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keystorePassword"));
assertEquals(ComponentRepresentation.SECRET_VALUE, createdRep.getConfig().getFirst("keyPassword"));
@ -109,16 +129,29 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
KeysMetadataRepresentation.KeyMetadataRepresentation key = keys.getKeys().get(0);
assertEquals(id, key.getProviderId());
assertEquals(AlgorithmType.RSA.name(), key.getType());
switch (algorithmType) {
case RSA: {
assertEquals(algorithmType.name(), key.getType());
PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), "RSA");
PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), "RSA");
assertEquals(exp, got);
break;
}
case ECDSA:
assertEquals("EC", key.getType());
PublicKey exp = PemUtils.decodePublicKey(generatedKeystore.getCertificateInfo().getPublicKey(), "EC");
PublicKey got = PemUtils.decodePublicKey(key.getPublicKey(), "EC");
assertEquals(exp, got);
}
assertEquals(priority, key.getProviderPriority());
assertEquals(generatedKeystore.getCertificateInfo().getPublicKey(), key.getPublicKey());
assertEquals(generatedKeystore.getCertificateInfo().getCertificate(), key.getCertificate());
}
@Test
public void invalidKeystore() throws Exception {
generateKeystore(KeystoreUtils.getPreferredKeystoreType());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keystore", "/nosuchfile");
Response response = adminClient.realm("test").components().add(rep);
@ -128,7 +161,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
@Test
public void invalidKeystorePassword() throws Exception {
generateKeystore(KeystoreUtils.getPreferredKeystoreType());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keystore", "invalid");
Response response = adminClient.realm("test").components().add(rep);
@ -138,7 +171,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
@Test
public void invalidKeyAlias() throws Exception {
generateKeystore(KeystoreUtils.getPreferredKeystoreType());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keyAlias", "invalid");
Response response = adminClient.realm("test").components().add(rep);
@ -158,7 +191,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
log.infof("Fallback to keystore type '%s' for the invalidKeyPassword() test", keystoreType);
}
generateKeystore(keystoreType);
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis());
ComponentRepresentation rep = createRep("valid", System.currentTimeMillis(), keyAlgorithm);
rep.getConfig().putSingle("keyPassword", "invalid");
Response response = adminClient.realm("test").components().add(rep);
@ -176,7 +209,7 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
response.close();
}
protected ComponentRepresentation createRep(String name, long priority) {
protected ComponentRepresentation createRep(String name, long priority, String algorithm) {
ComponentRepresentation rep = new ComponentRepresentation();
rep.setName(name);
rep.setParentId(adminClient.realm("test").toRepresentation().getId());
@ -188,11 +221,25 @@ public class JavaKeystoreKeyProviderTest extends AbstractKeycloakTest {
rep.getConfig().putSingle("keystorePassword", "password");
rep.getConfig().putSingle("keyAlias", "selfsigned");
rep.getConfig().putSingle("keyPassword", "password");
rep.getConfig().putSingle("algorithm", algorithm);
return rep;
}
private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType) throws Exception {
generateKeystore(keystoreType, AlgorithmType.RSA);
}
private void generateKeystore(KeystoreUtil.KeystoreFormat keystoreType, AlgorithmType algorithmType) throws Exception {
switch (algorithmType) {
case RSA: {
this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password");
this.keyAlgorithm = Algorithm.RS256;
return;
}
case ECDSA:
this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password", KeyUtils.generateECKey(Algorithm.ES256));
this.keyAlgorithm = Algorithm.ES256;
}
}
}