2

I have a client certificate composed of two files (.crt and .key) which I wish to import to a java KeyStore to then use in a SSLContext to sent HTTP requests with Apache's HTTPClient. However, I can't seem to find a way to do this programmatically, most other questions I've found either point to external tools or aren't fit for my case.

My certificate is encoded with the typical 'BEGIN CERTIFICATE' followed by a Base64 encoded string, and the key with 'BEGIN RSA PRIVATE KEY' and then another Base64 encoded string.

This is what I got so far:

private static SSLContext createSSLContext(File certFile, File keyFile) throws IOException {
    try {
        PEMParser pemParser = new PEMParser(new FileReader(keyFile));
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(new BouncyCastleProvider());
        Object object = pemParser.readObject();
        KeyPair kp = converter.getKeyPair((PEMKeyPair) object);
        PrivateKey privateKey = kp.getPrivate();

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        FileInputStream stream = new FileInputStream(certFile);
        X509Certificate cert = (X509Certificate) certFactory.generateCertificate(stream);

        KeyStore store = KeyStore.getInstance("JKS");
        store.load(null);
        store.setCertificateEntry("certificate", cert);
        store.setKeyEntry("private-key", privateKey, "changeit".toCharArray(), new Certificate[] { cert });

        SSLContext sslContext = SSLContexts.custom()
                .loadKeyMaterial(store, "changeit".toCharArray())
                .build();
        return sslContext;
    } catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException | KeyManagementException | UnrecoverableKeyException e) {
        throw new IOException(e);
    }
}

Stacktrace:

java.io.IOException: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format at me.failedshack.ssltest.SSLTest.createSSLContext(SSLTest.java:80) at me.failedshack.ssltest.SSLTest.main(SSLTest.java:31)

Caused by: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format at java.base/sun.security.rsa.RSAKeyFactory.engineGeneratePrivate(RSAKeyFactory.java:216) at java.base/java.security.KeyFactory.generatePrivate(KeyFactory.java:390) at me.failedshack.ssltest.SSLTest.createSSLContext(SSLTest.java:62) ... 1 more

Caused by: java.security.InvalidKeyException: invalid key format at java.base/sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:330) at java.base/sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:355) at java.base/sun.security.rsa.RSAPrivateCrtKeyImpl.(RSAPrivateCrtKeyImpl.java:91) at java.base/sun.security.rsa.RSAPrivateCrtKeyImpl.newKey(RSAPrivateCrtKeyImpl.java:75) at java.base/sun.security.rsa.RSAKeyFactory.generatePrivate(RSAKeyFactory.java:315) at java.base/sun.security.rsa.RSAKeyFactory.engineGeneratePrivate(RSAKeyFactory.java:212) ... 3 more

Sadly I keep getting an InvalidKeyException when generating the private key from the file.

FailedShack
  • 91
  • 4
  • 11
  • Stack trace please. NB You should not call `setCertificateEntry()`, only `setKeyEntry()`. – user207421 Jul 15 '18 at 22:51
  • And you also need to initialize the `SSLContext` with a `KeyManager`, not a `TrustManager`. Otherwise your private key will never be found. – user207421 Jul 15 '18 at 22:57
  • And you *also* can't cast a private key to a public key. Not much of this makes sense. – user207421 Jul 15 '18 at 22:59
  • @EJP Sorry about the PublicKey cast, it was something I was playing around with and accidentally copied over to the question. You're right about the KeyManager thing. I've modified the question. – FailedShack Jul 15 '18 at 23:14
  • Well this exception can only mean what it says: there is something wrong with the key file. Can you read it with OpenSSL? – user207421 Jul 16 '18 at 00:02
  • @EJP Just tried it and it didn't complain or anything. – FailedShack Jul 16 '18 at 00:46
  • @EJP: OpenSSL can transparently read several PEM formats for privatekey depending on type (4 for RSA), but Java unless you add BC can't read PEM at all and only one of the DER formats (PKCS8-unencrypted). – dave_thompson_085 Jul 16 '18 at 02:15

2 Answers2

4

A PEM file of type RSA PRIVATE KEY is base64 not binary and more importantly is in PKCS1 format NOT PKCS8 and thus cannot be processed as a PKCS8EncodedKeySpec.

Your choices are:

  • convert the PKCS1 PEM format to PKCS8 (unencrypted) PEM format; read that and drop the header and trailer lines and decode the base64 to binary and put that in PKCS8EncodedKeySpec -- but you say you don't want external tools, plus it's just as easy to convert the privatekey PLUS cert (or chain) into a PKCS12 (DER) which is already a Java keystore and avoid the issue

  • convert the PKCS1 PEM format to PKCS8 (unencrypted) DER format, which you can just read as binary and put in PKCS8EncodedKeySpec -- ditto

  • if the PKCS1 PEM is unencrypted, read and decode it as above to PKCS1 DER then manually construct the PKCS8 (unencrypted) encoding, and use that

  • if the PKCS1 PEM is encrypted, which you can detect because its body contains two 822-style header lines in addition to the base64, you have to replicate OpenSSL's 'legacy' key file decryption, PLUS construct the PKCS8 (unencrypted) encoding

  • if you can use BouncyCastle specifically bcpkix, it can directly read and parse all the PEM variants used by OpenSSL for privatekeys, including decrypting the encrypted ones; however, if you're not already using it, that's an additional jar to install and/or deploy

See one or more of these dupes:
Load certificate to KeyStore (JAVA) (Q constructs PKCS8 using BouncyCastle)
Java: Convert DKIM private key from RSA to DER for JavaMail (my answer constructs PKCS8 'by hand')
How to Load RSA Private Key From File (reads using BouncyCastle)
Read RSA private key of format PKCS1 in JAVA (reads using BouncyCastle)
Get a PrivateKey from a RSA .pem file (decrypt using BC)
Decrypting an OpenSSL PEM Encoded RSA private key with Java? (decrypt manually)
maybe PKCS#1 and PKCS#8 format for RSA private key (background)
and Differences between "BEGIN RSA PRIVATE KEY" and "BEGIN PRIVATE KEY" (background)

dave_thompson_085
  • 34,712
  • 6
  • 50
  • 70
  • Thank you so much for this clarification, I've opted for using BouncyCastle for reading the private key and seems to be doing its job. However, I can't get the requests to go through, I get the following exception: "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target". I've read that this means that the certificate is not trusted. The code I'm using is the same as in the question except for the few lines of code which I've changed to load the private key with BouncyCastle. – FailedShack Jul 17 '18 at 00:52
  • That's a different question; the privatekey+cert you load in a KeyManager to authenticate yourself is completely separate from and has nothing at all to do with TrustManager validating any other party (and vice versa). But your posted code appears to use a TMF without init'ing it, and that would not work at all and throw a quite different exception than you say. Please check your code, and (if you still have a problem) add to your Q what truststore you are using for what attempted connection. – dave_thompson_085 Jul 17 '18 at 11:11
  • I thought I had updated the code in the question, but apparently not. I'm trying to accomplish the same as here: https://github.com/Plailect/PlaiCDN/blob/master/PlaiCDN.py#L123 – FailedShack Jul 18 '18 at 12:50
  • Now I've tried manually importing the certificate and key to a PKCS12 key store with OpenSSL and loading it as explained here: https://stackoverflow.com/questions/21223084/how-do-i-use-an-ssl-client-certificate-with-apache-httpclient But keep getting the same message when connecting, I feel like I should open a new question at this point. – FailedShack Jul 18 '18 at 13:53
  • (1) OK, your code now validly uses (or rather lets JSSE use) the default truststore (2) the two hosts referenced in that python serve a leaf cert issued by 'Nintendo CA - G3'. That CA certainly isn't in the Java default CA-list, and although I don't know what store (your?) python uses I'd be quite surprised if that CA is in any standard distributed CA-list without modification. Are you sure you didn't (follow some instructions or setup procedure to) add that cert to the store on your system? – dave_thompson_085 Jul 18 '18 at 20:25
0

Using this small library https://github.com/KarlScheibelhofer/java-crypto-tools/tree/main you can load private keys and certificates in this PEM format easily. With much less code.

KeyStore ks = KeyStore.getInstance("pem", JctProvider.getInstance());
ks.load(new FileInputStream("webserver-key-and-certificate-chain.pem"), password);

See the Readme for examples and more details.