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
13 changed files with 546 additions and 117 deletions
@@ -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,19 +76,17 @@ 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 keyPair the key pair
* @param caPrivateKey the CA private key
* @param caCert the CA certificate
* @param subject the subject name
*
* @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,
String subject) throws Exception {
String subject) throws Exception {
try {
X500Name subjectDN = new X500Name("CN=" + subject);
@@ -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
*
* @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")
.setProvider(BouncyIntegration.PROVIDER);
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);
@@ -192,7 +202,7 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
@Override
public List<String> getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException {
Extensions certExtensions = new JcaX509CertificateHolder(cert).getExtensions();
if (certExtensions == null)
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate extensions were found");
@@ -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();
@@ -251,28 +262,26 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
}
public X509Certificate createServicesTestCertificate(String dn,
Date startDate,
Date expiryDate,
KeyPair keyPair,
String... certificatePolicyOid) {
Date startDate,
Date expiryDate,
KeyPair keyPair,
String... certificatePolicyOid) {
// Cert data
X500Name subjectDN = new X500Name(dn);
X500Name issuerDN = new X500Name(dn);
SubjectPublicKeyInfo subjPubKeyInfo = SubjectPublicKeyInfo.getInstance(
ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
BigInteger serialNumber = new BigInteger(130, new SecureRandom());
// Build the certificate
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate,
subjectDN, subjPubKeyInfo);
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);
@@ -282,11 +291,11 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
// Sign the cert with the private key
try {
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyIntegration.PROVIDER)
.build(keyPair.getPrivate());
.setProvider(BouncyIntegration.PROVIDER)
.build(keyPair.getPrivate());
X509Certificate x509Certificate = new JcaX509CertificateConverter()
.setProvider(BouncyIntegration.PROVIDER)
.getCertificate(certGen.build(contentSigner));
.setProvider(BouncyIntegration.PROVIDER)
.getCertificate(certGen.build(contentSigner));
return x509Certificate;
} catch (CertificateException | OperatorCreationException 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)));
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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;
@@ -51,7 +53,7 @@ public class ElytronECDSACryptoProvider implements ECDSACryptoProvider {
seq.endSequence();
return seq.getEncoded();
}
@Override
@@ -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);
@@ -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")
.setProvider(BouncyIntegration.PROVIDER);
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);
@@ -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);
}
}
}
@@ -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);
}
}
}