Skip to content

Commit

Permalink
Refactored SSLUtils.loadKey using PEMParser
Browse files Browse the repository at this point in the history
- Removed custom DER parsing and Base64 decoding in favor of Bouncy Castle PEM Parser
- Refactored SSLUtilsTest methods with reusable assertion method and algorithm checks
  • Loading branch information
exceptionfactory committed Nov 4, 2022
1 parent 7e9f9b7 commit 37cbbe0
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 213 deletions.
205 changes: 28 additions & 177 deletions util/src/main/java/io/kubernetes/client/util/SSLUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@
*/
package io.kubernetes.client.util;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
Expand All @@ -33,12 +31,10 @@
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.Collection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
Expand Down Expand Up @@ -117,54 +113,43 @@ public static String recognizePrivateKeyAlgo(byte[] privateKeyBytes) {
}

public static PrivateKey loadKey(byte[] privateKeyBytes)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
throws IOException, InvalidKeySpecException {
return loadKey(
new ByteArrayInputStream(privateKeyBytes), recognizePrivateKeyAlgo(privateKeyBytes));
}

public static PrivateKey loadKey(byte[] pemPrivateKeyBytes, String algo)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
throws IOException, InvalidKeySpecException {
return loadKey(new ByteArrayInputStream(pemPrivateKeyBytes), algo);
}

public static PrivateKey loadKey(InputStream keyInputStream, String clientKeyAlgo)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {

// Try PKCS7 / EC
if (clientKeyAlgo.equals("EC")) {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
PEMParser pemParser = new PEMParser(new InputStreamReader(keyInputStream));
Object pemObject;
while ((pemObject = pemParser.readObject()) != null) {
if (pemObject instanceof PEMKeyPair) {
return new JcaPEMKeyConverter().getKeyPair(((PEMKeyPair) pemObject)).getPrivate();
}
throws IOException, InvalidKeySpecException {
final PrivateKey privateKey;
try (final PEMParser pemParser = new PEMParser(new InputStreamReader(keyInputStream))) {
final Object pemObject = pemParser.readObject();
if (pemObject == null) {
final String message = String.format("PEM Private Key Algorithm [%s] not parsed", clientKeyAlgo);
throw new InvalidKeySpecException(message);
}
final JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
if (pemObject instanceof PEMKeyPair) {
final PEMKeyPair pemKeyPair = (PEMKeyPair) pemObject;
final KeyPair keyPair = converter.getKeyPair(pemKeyPair);
privateKey = keyPair.getPrivate();
} else if (pemObject instanceof PrivateKeyInfo) {
final PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) pemObject;
privateKey = converter.getPrivateKey(privateKeyInfo);
} else {
final String pemObjectType = pemObject.getClass().getSimpleName();
final String message = String.format("PEM Private Key Algorithm [%s] Type [%s] not supported",
clientKeyAlgo,
pemObjectType
);
throw new InvalidKeySpecException(message);
}
}

byte[] keyBytes = decodePem(keyInputStream);

// Try PKCS1 / RSA
if (clientKeyAlgo.equals("RSA")) {
RSAPrivateCrtKeySpec keySpec = decodePKCS1(keyBytes);
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
}

// Try PKCS8
// TODO: There _has_ to be a better way to do this, but I spent >
// 2 hours trying to find it and failed...
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
try {
return KeyFactory.getInstance("RSA").generatePrivate(spec);
} catch (InvalidKeySpecException ex) {
// ignore if it's not RSA
}
try {
return KeyFactory.getInstance("ECDSA").generatePrivate(spec);
} catch (InvalidKeySpecException ex) {
// ignore if it's not DSA
}
throw new InvalidKeySpecException("Unknown type of PKCS8 Private Key, tried RSA and ECDSA");
return privateKey;
}

public static KeyStore createKeyStore(
Expand Down Expand Up @@ -205,140 +190,6 @@ public static KeyStore createKeyStore(
return keyStore;
}

// This method is inspired and partly taken over from
// http://oauth.googlecode.com/svn/code/java/
// All credits to belong to them.
private static byte[] decodePem(InputStream keyInputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(keyInputStream));
try {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("-----BEGIN ")) {
return readBytes(reader, line.trim().replace("BEGIN", "END"));
}
}
throw new IOException("PEM is invalid: no begin marker");
} finally {
reader.close();
}
}

private static byte[] readBytes(BufferedReader reader, String endMarker) throws IOException {
String line;
StringBuffer buf = new StringBuffer();

while ((line = reader.readLine()) != null) {
if (line.indexOf(endMarker) != -1) {
return Base64.decodeBase64(buf.toString());
}
buf.append(line.trim());
}
throw new IOException("PEM is invalid : No end marker");
}

public static RSAPrivateCrtKeySpec decodePKCS1(byte[] keyBytes) throws IOException {
DerParser parser = new DerParser(keyBytes);
Asn1Object sequence = parser.read();
sequence.validateSequence();
parser = new DerParser(sequence.getValue());
parser.read();

return new RSAPrivateCrtKeySpec(
next(parser),
next(parser),
next(parser),
next(parser),
next(parser),
next(parser),
next(parser),
next(parser));
}

private static BigInteger next(DerParser parser) throws IOException {
return parser.read().getInteger();
}

static class DerParser {

private InputStream in;

DerParser(byte[] bytes) throws IOException {
this.in = new ByteArrayInputStream(bytes);
}

Asn1Object read() throws IOException {
int tag = in.read();

if (tag == -1) {
throw new IOException("Invalid DER: stream too short, missing tag");
}

int length = getLength();
byte[] value = new byte[length];
if (in.read(value) < length) {
throw new IOException("Invalid DER: stream too short, missing value");
}

return new Asn1Object(tag, value);
}

private int getLength() throws IOException {
int i = in.read();
if (i == -1) {
throw new IOException("Invalid DER: length missing");
}

if ((i & ~0x7F) == 0) {
return i;
}

int num = i & 0x7F;
if (i >= 0xFF || num > 4) {
throw new IOException("Invalid DER: length field too big (" + i + ")");
}

byte[] bytes = new byte[num];
if (in.read(bytes) < num) {
throw new IOException("Invalid DER: length too short");
}

return new BigInteger(1, bytes).intValue();
}
}

static class Asn1Object {

private final int type;
private final byte[] value;
private final int tag;

public Asn1Object(int tag, byte[] value) {
this.tag = tag;
this.type = tag & 0x1F;
this.value = value;
}

public byte[] getValue() {
return value;
}

BigInteger getInteger() throws IOException {
if (type != 0x02) {
throw new IOException("Invalid DER: object is not integer"); // $NON-NLS-1$
}
return new BigInteger(value);
}

void validateSequence() throws IOException {
if (type != 0x10) {
throw new IOException("Invalid DER: not a sequence");
}
if ((tag & 0x20) != 0x20) {
throw new IOException("Invalid DER: can't parse primitive entity");
}
}
}

private static void loadDefaultKeyStoreFile(KeyStore keyStore, char[] keyStorePassphrase)
throws CertificateException, NoSuchAlgorithmException, IOException {

Expand Down
99 changes: 63 additions & 36 deletions util/src/test/java/io/kubernetes/client/util/SSLUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,75 @@
package io.kubernetes.client.util;

import io.kubernetes.client.Resources;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.net.URL;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import junit.framework.TestCase;

public class SSLUtilsTest extends TestCase {

private static final String CLIENT_KEY_PATH =
new File(Resources.getResource("clientauth.key").getPath()).toString();
private static final String CLIENT_KEY_RSA_PATH =
new File(Resources.getResource("clientauth-rsa.key").getPath()).toString();
private static final String CLIENT_KEY_EC_PATH =
new File(Resources.getResource("clientauth-ec.key").getPath()).toString();

public void testPKCS8KeyLoadDump()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
byte[] loaded = Files.readAllBytes(Paths.get(CLIENT_KEY_PATH.replace("C:/", "")));
PrivateKey privateKey = SSLUtils.loadKey(loaded);
byte[] dumped = SSLUtils.dumpKey(privateKey);
PrivateKey reloaded = SSLUtils.loadKey(dumped);
assertEquals(privateKey, reloaded);
import org.apache.commons.io.IOUtils;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class SSLUtilsTest {

private static final String CLIENT_CERT = "clientauth.cert";

private static final String CLIENT_KEY_RSA_PKCS8 = "clientauth.key";

private static final String CLIENT_KEY_RSA_PKCS1 = "clientauth-rsa.key";

private static final String CLIENT_KEY_ECDSA_PKCS7 = "clientauth-ec.key";

private static final String CLIENT_KEY_ECDSA_PKCS8 = "clientauth-ec-fixed.key";

private static final String RSA_ALGORITHM = "RSA";

private static final String ECDSA_ALGORITHM = "ECDSA";

@Test
public void testLoadKeyRsaPkcs8() throws IOException, InvalidKeySpecException {
final PrivateKey privateKey = assertLoadDumpReloadKeyEquals(CLIENT_KEY_RSA_PKCS8);
assertEquals(RSA_ALGORITHM, privateKey.getAlgorithm());
}

public void testPKCS1RSAKeyLoadDump()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
byte[] loaded = Files.readAllBytes(Paths.get(CLIENT_KEY_RSA_PATH));
PrivateKey privateKey = SSLUtils.loadKey(loaded);
byte[] dumped = SSLUtils.dumpKey(privateKey);
PrivateKey reloaded = SSLUtils.loadKey(dumped);
assertEquals(privateKey, reloaded);
@Test
public void testLoadKeyRsaPkcs1() throws IOException, InvalidKeySpecException {
final PrivateKey privateKey = assertLoadDumpReloadKeyEquals(CLIENT_KEY_RSA_PKCS1);
assertEquals(RSA_ALGORITHM, privateKey.getAlgorithm());
}

@Test
public void testLoadKeyEcdsaPkcs7() throws IOException, InvalidKeySpecException {
final PrivateKey privateKey = assertLoadDumpReloadKeyEquals(CLIENT_KEY_ECDSA_PKCS7);
assertEquals(ECDSA_ALGORITHM, privateKey.getAlgorithm());
}

@Test
public void testLoadKeyEcdsaPkcs8() throws IOException, InvalidKeySpecException {
final PrivateKey privateKey = assertLoadDumpReloadKeyEquals(CLIENT_KEY_ECDSA_PKCS8);
assertEquals(ECDSA_ALGORITHM, privateKey.getAlgorithm());
}

@Test(expected = InvalidKeySpecException.class)
public void testLoadKeyCertificateNotSupported() throws IOException, InvalidKeySpecException {
final byte[] resourceBytes = getResourceBytes(CLIENT_CERT);
SSLUtils.loadKey(resourceBytes);
}

private PrivateKey assertLoadDumpReloadKeyEquals(final String filePath) throws IOException, InvalidKeySpecException {
final byte[] resourceBytes = getResourceBytes(filePath);
final PrivateKey privateKey = SSLUtils.loadKey(resourceBytes);

byte[] dumpedKey = SSLUtils.dumpKey(privateKey);
final PrivateKey reloadedPrivateKey = SSLUtils.loadKey(dumpedKey);

assertEquals(privateKey, reloadedPrivateKey);

return privateKey;
}

public void testPKCS1ECKeyLoadDump()
throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
byte[] loaded = Files.readAllBytes(Paths.get(CLIENT_KEY_EC_PATH));
PrivateKey privateKey = SSLUtils.loadKey(loaded);
byte[] dumped = SSLUtils.dumpKey(privateKey);
PrivateKey reloaded = SSLUtils.loadKey(dumped);
assertEquals(privateKey, reloaded);
private byte[] getResourceBytes(final String filePath) throws IOException {
final URL resourceUrl = Resources.getResource(filePath);
return IOUtils.toByteArray(resourceUrl);
}
}

0 comments on commit 37cbbe0

Please sign in to comment.