How to build a SSLSocketFactory from PEM certificate and key without converting to keystore?
Asked Answered
N

2

16

I'm given a self-signed client certificate kit that is to be used to access a server via HTTPS. The kit consists of the following PEM files:

  1. client.crt (client certificate)
  2. client.key (client private key)
  3. ca.crt (CA certificate)

One way to solve the task is to generate a Java keystore:

  1. Use openssl to convert client certificate and key to PKCS12 keystore
  2. Use keytool to import CA certificate to the store

... and then use code like the following to build SSLSocketFactory instance:

InputStream stream = new ByteArrayInputStream(pksData);         
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(stream, password);

KeyManagerFactory kmf = KeyManagerFactory.getInstance(
    KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password.toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();

TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
    TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(keyStore);
TrustManager[] trustManagers = tmfactory.getTrustManagers();

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
sslSocketFactory = sslContext.getSocketFactory();

... which is later used to init http library.

So we obtain a KeyStore, then init KeyManagers and TrustManagers with its help and finally we build SSLSocketFactory instance with them.

The question is: is there a way to avoid that keystore file creation and somehow build SSLSocketFactory starting with PublicKey and Certificate instance (which, for example, can be obtained from PEM files using bouncycastle's PemReader)?

Nadaha answered 8/3, 2017 at 15:14 Comment(2)
No. You have to build a PKCS#12 or JKS KeyStore. But you should nit have been given a private key. You should have generated it all yourself. There is a serious security problem here. Your private key is not private, so whoever gave it to you can impersonate you in the legal sense. Don't do this.Psychro
Yes, thank you, we know that we should generate private keys ourselves. But the institution with which we work here dictates its rules and does not listen to anyone: they just generate all the keys themselves. It's not a technical matter, though.Nadaha
N
15

It turned out that a KeyStore instance still has to be built, but it can be done in memory (starting with PEM files as input), without using an intermediate keystore file build with keytool.

To build that in-memory KeyStore, code like the following may be used:

private static final String TEMPORARY_KEY_PASSWORD = "changeit";

private KeyStore getKeyStore() throws ConfigurationException {
    try {
        Certificate clientCertificate = loadCertificate(certificatePem);
        PrivateKey privateKey = loadPrivateKey(privateKeyPem);
        Certificate caCertificate = loadCertificate(caPem);

        KeyStore keyStore = KeyStore.getInstance("JKS");
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca-cert", caCertificate);
        keyStore.setCertificateEntry("client-cert", clientCertificate);
        keyStore.setKeyEntry("client-key", privateKey, TEMPORARY_KEY_PASSWORD.toCharArray(), new Certificate[]{clientCertificate});
        return keyStore;
    } catch (GeneralSecurityException | IOException e) {
        throw new ConfigurationException("Cannot build keystore", e);
    }
}

private Certificate loadCertificate(String certificatePem) throws IOException, GeneralSecurityException {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
    final byte[] content = readPemContent(certificatePem);
    return certificateFactory.generateCertificate(new ByteArrayInputStream(content));
}

private PrivateKey loadPrivateKey(String privateKeyPem) throws IOException, GeneralSecurityException {
    return pemLoadPrivateKeyPkcs1OrPkcs8Encoded(privateKeyPem);
}

private byte[] readPemContent(String pem) throws IOException {
    final byte[] content;
    try (PemReader pemReader = new PemReader(new StringReader(pem))) {
        final PemObject pemObject = pemReader.readPemObject();
        content = pemObject.getContent();
    }
    return content;
}

private static PrivateKey pemLoadPrivateKeyPkcs1OrPkcs8Encoded(
        String privateKeyPem) throws GeneralSecurityException, IOException {
    // PKCS#8 format
    final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----";
    final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----";

    // PKCS#1 format
    final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----";
    final String PEM_RSA_PRIVATE_END = "-----END RSA PRIVATE KEY-----";

    if (privateKeyPem.contains(PEM_PRIVATE_START)) { // PKCS#8 format
        privateKeyPem = privateKeyPem.replace(PEM_PRIVATE_START, "").replace(PEM_PRIVATE_END, "");
        privateKeyPem = privateKeyPem.replaceAll("\\s", "");

        byte[] pkcs8EncodedKey = Base64.getDecoder().decode(privateKeyPem);

        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(new PKCS8EncodedKeySpec(pkcs8EncodedKey));

    } else if (privateKeyPem.contains(PEM_RSA_PRIVATE_START)) {  // PKCS#1 format

        privateKeyPem = privateKeyPem.replace(PEM_RSA_PRIVATE_START, "").replace(PEM_RSA_PRIVATE_END, "");
        privateKeyPem = privateKeyPem.replaceAll("\\s", "");

        DerInputStream derReader = new DerInputStream(Base64.getDecoder().decode(privateKeyPem));

        DerValue[] seq = derReader.getSequence(0);

        if (seq.length < 9) {
            throw new GeneralSecurityException("Could not parse a PKCS1 private key.");
        }

        // skip version seq[0];
        BigInteger modulus = seq[1].getBigInteger();
        BigInteger publicExp = seq[2].getBigInteger();
        BigInteger privateExp = seq[3].getBigInteger();
        BigInteger prime1 = seq[4].getBigInteger();
        BigInteger prime2 = seq[5].getBigInteger();
        BigInteger exp1 = seq[6].getBigInteger();
        BigInteger exp2 = seq[7].getBigInteger();
        BigInteger crtCoef = seq[8].getBigInteger();

        RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2,
                exp1, exp2, crtCoef);

        KeyFactory factory = KeyFactory.getInstance("RSA");

        return factory.generatePrivate(keySpec);
    }

    throw new GeneralSecurityException("Not supported format of a private key");
}

The idea is taken from Programmatically Obtain KeyStore from PEM

Nadaha answered 11/3, 2017 at 10:4 Comment(2)
Thank you, this solution works well for java 8 however the DerInputStream and DerValue are not available anymore within java 11. Any idea what would be a proper alternative for parsing the content of -----BEGIN RSA PRIVATE KEY----- * -----END RSA PRIVATE KEY-----Teofilateosinte
@Teofilateosinte I'm not sure if this is ok from the legal point of view, but technically you could just copy the whole sun.security.util package from Java 8 to your own package. Also there is github.com/AdoptOpenJDK/openjdk-jdk11/blob/master/src/java.base/… , you could just copy the code from there (or use AdoptOpenJDK, probably?)Nadaha
T
5

I dropped earlier a comment on your answer when facing a similar challenge and now I came back to provide an alternative for loading the pem file. I have created a library out of it to make it easier for myself and others, see here: GitHub - SSLContext Kickstart I hope you like it :)

Add the following dependency:

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>sslcontext-kickstart-for-pem</artifactId>
    <version>8.0.0</version>
</dependency>

The pem files can be loaded with the following snippet:

var keyManager = PemUtils.loadIdentityMaterial("certificate-chain.pem", "private-key.pem");
var trustManager = PemUtils.loadTrustMaterial("some-trusted-certificate.pem");

var sslFactory = SSLFactory.builder()
          .withIdentityMaterial(keyManager)
          .withTrustMaterial(trustManager)
          .build();

var sslContext = sslFactory.getSslContext();
var sslSocketFactory = sslFactory.getSslSocketFactory();

Coming back to your main question, I also discovered that it is not possible to create a SSLSocketFactory without the KeyStores. And an in-memory KeyStore works perfectly as you suggested for this use case.

Teofilateosinte answered 21/2, 2021 at 0:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.