diff --git a/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java b/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java index 4a63c0b385..1294d6dc64 100644 --- a/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java @@ -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); } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java index e7aa605f86..d731bde9f3 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java @@ -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 Bill Burke * @author Giriraj Sharma @@ -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 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 CRL validation + * * @param cert * @return * @throws IOException @@ -225,7 +236,7 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider { List 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 certPolicyExtensions(String... certificatePolicyOid) { List certificatePolicies = new LinkedList<>(); - if (certificatePolicyOid != null && certificatePolicyOid.length > 0) - { + if (certificatePolicyOid != null && certificatePolicyOid.length > 0) { List policyInfoList = new LinkedList<>(); - for (String oid: certificatePolicyOid) - { + for (String oid : certificatePolicyOid) { policyInfoList.add(new PolicyInformation(new ASN1ObjectIdentifier(oid))); } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java index 9cf773175b..142380f9d6 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java @@ -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); + } + } + } diff --git a/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCECDSACryptoProviderTest.java b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCECDSACryptoProviderTest.java new file mode 100644 index 0000000000..15afc3f253 --- /dev/null +++ b/crypto/default/src/test/java/org/keycloak/crypto/def/test/BCECDSACryptoProviderTest.java @@ -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 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); + } + } +} \ No newline at end of file diff --git a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java index b5f296ebc3..855de2d3ed 100644 --- a/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java +++ b/crypto/elytron/src/main/java/org/keycloak/crypto/elytron/ElytronECDSACryptoProvider.java @@ -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); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java index 0f505d9218..d8344564de 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java @@ -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 Bill Burke * @author Giriraj Sharma @@ -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); diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java index 1b6dfd35b7..401bb5d352 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java @@ -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); + } + } + + } diff --git a/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java new file mode 100644 index 0000000000..79e58f9276 --- /dev/null +++ b/crypto/fips1402/src/test/java/org/keycloak/crypto/fips/test/BCFIPSECDSACryptoProviderTest.java @@ -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 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); + } + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java index 2d3b9433a1..6dafffa218 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProvider.java @@ -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 Stian Thorgersen */ -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 loadCertificateChain(KeyStore keyStore, String keyAlias) throws GeneralSecurityException { List 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 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; + } + /** *

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 getKeysStream() { + return Stream.of(key); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java index e23cca0ff3..ea2f9b1651 100644 --- a/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java +++ b/services/src/main/java/org/keycloak/keys/JavaKeystoreKeyProviderFactory.java @@ -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 Stian Thorgersen */ -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 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 ecAlgorithms = List.of(Algorithm.ES256, Algorithm.ES384, Algorithm.ES512); + List 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; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java index 3afeeac700..b88b3461d3 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeyUtils.java @@ -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}. */ diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java index 4615118050..240b780861 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/KeystoreUtils.java @@ -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,16 +63,15 @@ 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); keyStore.load(null, null); - Certificate[] chain = {certificate}; + Certificate[] chain = {certificate}; keyStore.setKeyEntry(subject, keyPair.getPrivate(), keyPassword.trim().toCharArray(), chain); File file = folder.newFile(fileName); @@ -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; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java index e5f123b48a..4ed7b31d36 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/keys/JavaKeystoreKeyProviderTest.java @@ -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 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 { - this.generatedKeystore = KeystoreUtils.generateKeystore(folder, keystoreType, "selfsigned", "password", "password"); + 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; + } } }