From eea1acbc1dcb58ec3fd847591b17fc132b36136f Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Wed, 12 Jun 2024 07:55:26 -0400 Subject: [PATCH] fix: remove bc optional dependency closes: #6008 Signed-off-by: Steve Hawkins --- CHANGELOG.md | 1 + doc/FAQ.md | 10 -- junit/kube-api-test/core/pom.xml | 6 ++ junit/kubernetes-junit-jupiter/pom.xml | 10 -- kubernetes-client-api/pom.xml | 2 - .../kubernetes/client/internal/CertUtils.java | 66 +++---------- .../kubernetes/client/internal/PKCS1Util.java | 98 ++++++++++++++++++- .../client/internal/CertUtilsTest.java | 6 +- kubernetes-client/pom.xml | 10 -- kubernetes-itests/pom.xml | 10 ++ kubernetes-tests/pom.xml | 9 -- pom.xml | 4 +- 12 files changed, 134 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6314bcb97d3..cd23235ef7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ #### Bugs #### Improvements +* Fix #6008: removing the optional dependency on bouncy castle #### Dependency Upgrade diff --git a/doc/FAQ.md b/doc/FAQ.md index 827c25fbf95..b361ed4bf93 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -18,16 +18,6 @@ By default kubernetes-client has a runtime dependency on OkHttp (kubernetes-http If you wish to use another HttpClient implementation typically you will exclude kubernetes-httpclient-okhttp and include the other runtime or compile dependency instead. -### What is Bouncy Castle Optional dependency and When is it required? -[BouncyCastle](https://bouncycastle.org/) is a Java library that complements the default Java Cryptographic Extension (JCE) and it is required for using some KubernetesClient features. To use support for EC Keys you must explicitly add this dependency to classpath. For example, in case of a Maven project add the required dependencies to `pom.xml` file such as: -```xml - - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} - -``` - ### I've tried adding a dependency to kubernetes-client, but I'm still getting weird class loading issues, what gives? More than likely your project already has transitive dependencies to a conflicting version of the Fabric8 Kubernetes Client. For example spring-cloud-dependencies already depends upon the client. You should fully override the client version in this case via the kubernetes-client-bom: diff --git a/junit/kube-api-test/core/pom.xml b/junit/kube-api-test/core/pom.xml index ee484744fd4..d6b448c15e3 100644 --- a/junit/kube-api-test/core/pom.xml +++ b/junit/kube-api-test/core/pom.xml @@ -95,6 +95,12 @@ org.bouncycastle bcpkix-jdk18on + compile + + + org.bouncycastle + bcprov-jdk18on + compile io.javaoperatorsdk diff --git a/junit/kubernetes-junit-jupiter/pom.xml b/junit/kubernetes-junit-jupiter/pom.xml index f26bc9ca483..9917078e32c 100644 --- a/junit/kubernetes-junit-jupiter/pom.xml +++ b/junit/kubernetes-junit-jupiter/pom.xml @@ -40,16 +40,6 @@ commons-compress false - - org.bouncycastle - bcprov-jdk18on - false - - - org.bouncycastle - bcpkix-jdk18on - false - org.junit.jupiter junit-jupiter-api diff --git a/kubernetes-client-api/pom.xml b/kubernetes-client-api/pom.xml index e6c4e34e992..021785f00fa 100644 --- a/kubernetes-client-api/pom.xml +++ b/kubernetes-client-api/pom.xml @@ -160,12 +160,10 @@ org.bouncycastle bcprov-jdk18on - true org.bouncycastle bcpkix-jdk18on - true diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java index 33269dfa815..4c96c846d30 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/CertUtils.java @@ -15,13 +15,7 @@ */ package io.fabric8.kubernetes.client.internal; -import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.utils.Utils; -import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openssl.PEMKeyPair; -import org.bouncycastle.openssl.PEMParser; -import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +34,6 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; @@ -52,7 +45,6 @@ import java.util.Base64; import java.util.Collection; import java.util.Collections; -import java.util.concurrent.Callable; import java.util.stream.Collectors; public class CertUtils { @@ -168,56 +160,30 @@ private static PrivateKey loadKey(InputStream keyInputStream, String clientKeyAl if (clientKeyAlgo == null) { clientKeyAlgo = "RSA"; // by default let's assume it's RSA } - if (clientKeyAlgo.equals("EC")) { - return handleECKey(keyInputStream); - } else if (clientKeyAlgo.equals("RSA")) { - return handleOtherKeys(keyInputStream, clientKeyAlgo); + byte[] keyBytes = decodePem(keyInputStream); + if (clientKeyAlgo.equals("EC") || clientKeyAlgo.equals("RSA")) { + try { + return handleOtherKeys(keyBytes, clientKeyAlgo); + } catch (IOException e) { + // could be a version 1 key + if (clientKeyAlgo.equals("EC")) { + return handleECKey(keyBytes); + } else { + throw e; + } + } } throw new InvalidKeySpecException("Unknown type of PKCS8 Private Key, tried RSA and ECDSA"); } - private static PrivateKey handleECKey(InputStream keyInputStream) { - // Let's wrap the code to a callable inner class to avoid NoClassDef when loading this class. - try { - return new Callable() { - @Override - public PrivateKey call() throws IOException { - if (Security.getProvider("BC") == null && Security.getProvider("BCFIPS") == null) { - // org.bouncycastle.jce.provider.BouncyCastleProvider needs to be wrapped with a Callable otherwise - // runtime won't even evaluate this whole block. This happens even when above condition testing if - // block evaluates to false - new Callable() { - @Override - public String call() { - Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); - return null; - } - }.call(); - } - Object pemObject = new PEMParser(new InputStreamReader(keyInputStream)).readObject(); - if (pemObject == null) { - throw new KubernetesClientException("Got null PEM object from EC key's input stream."); - } else if (pemObject instanceof PEMKeyPair) { - return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) pemObject).getPrivate(); - } else if (pemObject instanceof PrivateKeyInfo) { - return BouncyCastleProvider.getPrivateKey((PrivateKeyInfo) pemObject); - } else { - throw new KubernetesClientException("Don't know what to do with a " + pemObject.getClass().getName()); - } - } - }.call(); - } catch (NoClassDefFoundError e) { - throw new KubernetesClientException( - "JcaPEMKeyConverter is provided by BouncyCastle, an optional dependency. To use support for EC Keys you must explicitly add this dependency to classpath."); - } catch (IOException e) { - throw new KubernetesClientException(e.getMessage()); - } + private static PrivateKey handleECKey(byte[] keyBytes) + throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + return KeyFactory.getInstance("EC").generatePrivate(PKCS1Util.getECKeySpec(keyBytes)); } - private static PrivateKey handleOtherKeys(InputStream keyInputStream, String clientKeyAlgo) + private static PrivateKey handleOtherKeys(byte[] keyBytes, String clientKeyAlgo) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { - byte[] keyBytes = decodePem(keyInputStream); try { // First let's try PKCS8 return KeyFactory.getInstance(clientKeyAlgo).generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/PKCS1Util.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/PKCS1Util.java index dd22eafe02f..a9f855c54ee 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/PKCS1Util.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/PKCS1Util.java @@ -15,8 +15,20 @@ */ package io.fabric8.kubernetes.client.internal; -import java.io.*; +import io.fabric8.kubernetes.client.KubernetesClientException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPairGenerator; +import java.security.Provider; +import java.security.Security; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; import java.security.spec.RSAPrivateCrtKeySpec; /** @@ -57,9 +69,12 @@ private static BigInteger next(DerParser parser) throws IOException { static class DerParser { + private final static int SEQUENCE = 0x10; + private final static int INTEGER = 0x02; + private final static int OBJECT_IDENTIFIER = 0x06; private InputStream in; - DerParser(byte[] bytes) throws IOException { + DerParser(byte[] bytes) { this.in = new ByteArrayInputStream(bytes); } @@ -136,4 +151,83 @@ void validateSequence() throws IOException { } } } + + // adapted from io.vertx.core.net.impl.pkcs1.PrivateKeyParser + + public static ECPrivateKeySpec getECKeySpec(byte[] keyBytes) throws IOException { + DerParser parser = new DerParser(keyBytes); + + Asn1Object sequence = parser.read(); + if (sequence.type != DerParser.SEQUENCE) { + throw new KubernetesClientException("Invalid DER: not a sequence"); + } + + // Parse inside the sequence + parser = new DerParser(sequence.value); + + Asn1Object version = parser.read(); + if (version.type != DerParser.INTEGER) { + throw new KubernetesClientException(String.format( + "Invalid DER: 'version' field must be of type INTEGER (2) but found type `%d`", + version.type)); + } else if (version.getInteger().intValue() != 1) { + throw new KubernetesClientException(String.format( + "Invalid DER: expected 'version' field to have value '1' but found '%d'", + version.getInteger().intValue())); + } + byte[] privateValue = parser.read().getValue(); + parser = new DerParser(parser.read().getValue()); + Asn1Object params = parser.read(); + // ECParameters are mandatory according to RFC 5915, Section 3 + if (params.type != DerParser.OBJECT_IDENTIFIER) { + throw new KubernetesClientException(String.format( + "Invalid DER: expected to find an OBJECT_IDENTIFIER (6) in 'parameters' but found type '%d'", + params.type)); + } + byte[] namedCurveOid = params.getValue(); + ECParameterSpec spec = getECParameterSpec(oidToString(namedCurveOid)); + return new ECPrivateKeySpec(new BigInteger(1, privateValue), spec); + } + + private static ECParameterSpec getECParameterSpec(String curveName) { + Provider[] providers = Security.getProviders(); + GeneralSecurityException ex = null; + // scan through the providers to see if anyone supports this + for (Provider provider : providers) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", provider); + keyPairGenerator.initialize(new ECGenParameterSpec(curveName)); + ECPublicKey publicKey = (ECPublicKey) keyPairGenerator.generateKeyPair().getPublic(); + return publicKey.getParams(); + } catch (GeneralSecurityException e) { + ex = e; + } + } + boolean bcProvider = Security.getProvider("BC") != null || Security.getProvider("BCFIPS") != null; + throw new KubernetesClientException("Cannot determine EC parameter spec for curve name/OID" + (bcProvider ? "" + : ". A BouncyCastle provider is not installed, it may be needed for this EC algorithm."), ex); + } + + private static String oidToString(byte[] oid) { + StringBuilder result = new StringBuilder(); + int value = oid[0] & 0xff; + result.append(value / 40).append(".").append(value % 40); + for (int index = 1; index < oid.length; ++index) { + byte bValue = oid[index]; + if (bValue < 0) { + value = (bValue & 0b01111111); + ++index; + if (index == oid.length) { + throw new IllegalArgumentException("Invalid OID"); + } + value <<= 7; + value |= (oid[index] & 0b01111111); + result.append(".").append(value); + } else { + result.append(".").append(bValue); + } + } + return result.toString(); + } + } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java index 767f3dacfe0..e296f287ead 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/CertUtilsTest.java @@ -214,9 +214,9 @@ void loadNothingError() { String privateKeyPath = Utils.filePath(getClass().getResource("/ssl-test/empty")); String certPath = Utils.filePath(getClass().getResource("/ssl-test/empty")); - assertThatExceptionOfType(KubernetesClientException.class) + assertThatExceptionOfType(IOException.class) .isThrownBy(() -> CertUtils.createKeyStore(null, certPath, null, privateKeyPath, "EC", "foo", null, null)) - .withMessage("Got null PEM object from EC key's input stream."); + .withMessage("PEM is invalid: no begin marker"); } @Test @@ -226,7 +226,7 @@ void loadUnknownError() { assertThatExceptionOfType(KubernetesClientException.class) .isThrownBy(() -> CertUtils.createKeyStore(null, certPath, null, privateKeyPath, "EC", "foo", null, null)) - .withMessageContaining("Don't know what to do with a"); + .withMessageContaining("Invalid DER"); } @Test diff --git a/kubernetes-client/pom.xml b/kubernetes-client/pom.xml index 176310ae1db..20a2b613cf8 100644 --- a/kubernetes-client/pom.xml +++ b/kubernetes-client/pom.xml @@ -77,16 +77,6 @@ ${zjsonpatch.version} - - org.bouncycastle - bcprov-jdk18on - true - - - org.bouncycastle - bcpkix-jdk18on - true - org.apache.commons commons-compress diff --git a/kubernetes-itests/pom.xml b/kubernetes-itests/pom.xml index 3ee1647381e..90acaba271f 100644 --- a/kubernetes-itests/pom.xml +++ b/kubernetes-itests/pom.xml @@ -68,6 +68,16 @@ slf4j-simple test + + org.bouncycastle + bcprov-jdk18on + test + + + org.bouncycastle + bcpkix-jdk18on + test + diff --git a/kubernetes-tests/pom.xml b/kubernetes-tests/pom.xml index ae0ac66bd05..4b49ddc5b87 100644 --- a/kubernetes-tests/pom.xml +++ b/kubernetes-tests/pom.xml @@ -94,15 +94,6 @@ org.awaitility awaitility - - - org.bouncycastle - bcprov-jdk18on - - - org.bouncycastle - bcpkix-jdk18on - diff --git a/pom.xml b/pom.xml index f2c2f3153ad..f99500ae965 100644 --- a/pom.xml +++ b/pom.xml @@ -806,13 +806,13 @@ org.bouncycastle bcprov-jdk18on ${bouncycastle.version} - true + test org.bouncycastle bcpkix-jdk18on ${bouncycastle.version} - true + test org.eclipse.jetty