Skip to content

Commit

Permalink
fix: remove bc optional dependency
Browse files Browse the repository at this point in the history
closes: fabric8io#6008

Signed-off-by: Steve Hawkins <[email protected]>
  • Loading branch information
shawkins committed Jun 12, 2024
1 parent 456c810 commit 3c89132
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 109 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#### Bugs

#### Improvements
* Fix #6008: removing the optional dependency on bouncy castle

#### Dependency Upgrade

Expand Down
10 changes: 0 additions & 10 deletions doc/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
```

### 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:
Expand Down
7 changes: 0 additions & 7 deletions httpclient-vertx/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,6 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!-- Required by SslTest -->
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
4 changes: 0 additions & 4 deletions junit/kube-api-test/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,6 @@
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>kubernetes-webhooks-framework-core</artifactId>
Expand Down
10 changes: 0 additions & 10 deletions junit/kubernetes-junit-jupiter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,6 @@
<artifactId>commons-compress</artifactId>
<optional>false</optional>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<optional>false</optional>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<optional>false</optional>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
2 changes: 0 additions & 2 deletions kubernetes-client-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,10 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<optional>true</optional>
</dependency>

<!-- Compile Only Dependencies -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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<PrivateKey>() {
@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<String>() {
@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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 0 additions & 10 deletions kubernetes-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,6 @@
<version>${zjsonpatch.version}</version>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
Expand Down
9 changes: 0 additions & 9 deletions kubernetes-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,6 @@
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>

<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -806,13 +806,13 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
<optional>true</optional>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
Expand Down

0 comments on commit 3c89132

Please sign in to comment.