Search code examples
javaclient-certificatespkipkcs#12aws-acm

Convert ACM exported certificate to p12 file in Java


I'm building an MTLS authentication system where the registration step will generate a certificate using AWS ACM private CA.

So in the registration step, I use AWS ACM SDK to first generate a certificate and then export it.

RequestCertificateResult requestCertificateResult = client.requestCertificate(request);
String certificateArn = requestCertificateResult.getCertificateArn();

ExportCertificateRequest exportRequest = new ExportCertificateRequest();
exportRequest.setCertificateArn(certificateArn);
exportRequest.setPassphrase(ByteBuffer.wrap(password.getBytes()));

ExportCertificateResult exportCertificateResult = client.exportCertificate(exportRequest);

String certificateChain = exportCertificateResult.getCertificateChain();
String certificate = exportCertificateResult.getCertificate();
String privateKey = exportCertificateResult.getPrivateKey();

What I would like to return to the client is a .p12 file, including the certificates and the privateKey. However, the result from ACM contain certificate, certificateChain and privateKey as Strings. How can I convert them into a .p12 file using Java? Everything I find on the internet is using openssl, but since this will be part of an automated registration step I need to convert it programmatically.

Any suggestions or pointers in the right direction is greatly appreciated.

Edit! This is what I want to do, but in Java:

openssl pkcs12 -export -inkey private_key.txt -in certificate.txt -certfile certificate_chain.txt -out final_result.p12

And using strings instead of .txt files.


Solution

  • If https://docs.aws.amazon.com/acm/latest/APIReference/API_ExportCertificate.html is what you are using, it describes and shows these values as being in PEM formats, which is consistent with your use in openssl pkcs12 -export. However the spec says that privatekey is labelled BEGIN/END PRIVATE KEY which is PKCS8-unencrypted, while the example shows ENCRYPTED PRIVATE KEY which is (bet you couldn't guess!) PKCS8-encrypted, although the example has clear errors that make me distrust it.

    The leaf certificate and chain are easy, just feed them to CertificateFactory which can handle either PEM or 'DER' (binary). To put them in a Java keystore (of any format), you need to combine them in a single array, leaf-then-chain:

    // note: use java.security.cert.Certificate, not obsoleted java.security.Certificate or javax.security.cert.Certificate 
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    Certificate leaf = cf.generateCertificate(new ByteArrayInputStream(certString.getBytes()));
    Collection<Certificate> chain = cf.generateCertificates(new ByteArrayInputStream(chainString.getBytes()));
    // for general data String.getBytes() omitting/defaulting charset 
    // can be dangerous, but PEM data is a strict subset of ASCII and safe
    Certificate[] combine = chain.toArray( new Certificate[chain.size()+1] );
    System.arraycopy(combine,0,combine,1,combine.length-1);
    combine[0] = leaf;
    

    or it can be easier to combine the inputs, but the specs indicate they might not provide the final linebreak (on the PEM END line) of the cert; if so you must add it to have a valid PEM sequence:

    String temp = certString.endsWith("\n")? certString: certString + "\n";
    Certificate[] combine = CertificateFactory.getInstance("X.509")
        .generateCertificates(new ByteArrayInputStream(temp+chainString))
        .toArray(new Certificate[0]);
    

    If the privatekey is in fact PKCS8-unencrypted, it's almost as easy. KeyFactory handles that, but only as 'DER' not PEM, so you need to undo the PEM 'wrapping'. One popular way is

    String justb64 = privkeyString.replaceAll("-----(BEGIN|END) PRIVATE KEY-----","").replaceAll("\\r?\\n","");
    byte[] binary = Base64.getDecoder().decode(justb64);
    // or can leave the linebreaks in the data and use getMimeDecoder()
    // prior to j8 other base64 decoders like Apache commons were popular,
    // although in a pinch you can write your own by hand
    
    // Java wants to know the algorithm of the key _before_ parsing it;
    // since we have a known-matching cert, we can use that
    PrivateKey pkey = KeyFactory.getInstance(combine[0].getPublicKey().getAlgorithm())
       .generatePrivate(new PKCS8EncodedKeySpec(binary));
    

    Another method is something like

    String[] lines = privkeyString.split("\r?\n"); 
    // or if reading from a file use BufferedReader or nio.Files.readAllLines
    // may want to check that lines[0] and lines[lines.length-1] are in fact
    // the desired BEGIN and END lines, if there is any chance the data is wrong
    String justb64 = String.join("",Arrays.copyOfRange(lines,1,lines.length-1));
    // continue as above
    

    You can now put these in a PKCS12 keystore with

    KeyStore p12 = KeyStore.getInstance("PKCS12"); p12.load(null);
    p12.setKeyEntry(alias, privkey, password, combine);
    p12.store(/*OutputStream to desired file or other writable location*/, password);
    

    If the privatekey is in fact encrypted, standard Java can't easily read it as a key. However, if it is encrypted with one of the algorithms supported by (your) Java in a PKCS12 store, as a bypass you can use the 'pre-protected' API:

    byte[] priv_pkcs8enc = // un-PEM privkeyString as before, but _don't_ pass to KeyFactory
    ...
    p12.setKeyEntry(alias, priv_pkcs8enc, combine);
    

    I say 'your' Java, because the set of PKCS8 encryptions supported by Java has varied over time (versions) and across implementations (i.e. providers), and I suspect will continue to.

    If the privatekey is encrypted with an algorithm not supported in PKCS12 by your Java, you're probably out of luck with standard (Oracle, OpenJDK, etc) Java. But if you want to pursue that, add to your Q details of a test key -- bearing in mind if Amazon doesn't document what algorithm(s?) they use, it might vary (perhaps across regions or service offerings) and might change in the future.