From d95b2c6774e96279a4667fb353c6b719d35493be 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 --- httpclient-vertx/pom.xml | 7 -- junit/kube-api-test/core/pom.xml | 4 - junit/kubernetes-junit-jupiter/pom.xml | 10 --- kubernetes-client-api/pom.xml | 2 - .../kubernetes/client/internal/CertUtils.java | 48 +--------- .../kubernetes/client/internal/PKCS1Util.java | 88 ++++++++++++++++++- kubernetes-client/pom.xml | 10 --- kubernetes-tests/pom.xml | 9 -- pom.xml | 4 +- 11 files changed, 93 insertions(+), 100 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/httpclient-vertx/pom.xml b/httpclient-vertx/pom.xml index 998602a628d..b365f5c9dfa 100644 --- a/httpclient-vertx/pom.xml +++ b/httpclient-vertx/pom.xml @@ -91,13 +91,6 @@ assertj-core test - - - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} - test - diff --git a/junit/kube-api-test/core/pom.xml b/junit/kube-api-test/core/pom.xml index ee484744fd4..44836f82795 100644 --- a/junit/kube-api-test/core/pom.xml +++ b/junit/kube-api-test/core/pom.xml @@ -92,10 +92,6 @@ jetty-server test - - org.bouncycastle - bcpkix-jdk18on - io.javaoperatorsdk kubernetes-webhooks-framework-core 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..70f735f2f39 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 { @@ -177,42 +169,10 @@ private static PrivateKey loadKey(InputStream keyInputStream, String clientKeyAl 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(InputStream keyInputStream) + throws IOException, InvalidKeySpecException, NoSuchAlgorithmException { + byte[] keyBytes = decodePem(keyInputStream); + return KeyFactory.getInstance("EC").generatePrivate(PKCS1Util.getECKeySpec(keyBytes)); } private static PrivateKey handleOtherKeys(InputStream keyInputStream, String clientKeyAlgo) 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..3b943396f36 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,18 @@ */ 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.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; import java.security.spec.RSAPrivateCrtKeySpec; /** @@ -57,9 +67,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 +149,75 @@ 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) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + keyPairGenerator.initialize(new ECGenParameterSpec(curveName)); + ECPublicKey publicKey = (ECPublicKey) keyPairGenerator.generateKeyPair().getPublic(); + return publicKey.getParams(); + } catch (GeneralSecurityException e) { + throw new KubernetesClientException("Cannot determine EC parameter spec for curve name/OID", e); + } + } + + 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/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-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