Converting PFX format to PEM via OpenSSL programmatically
I run into a task that I needed to do in Go, given a PFX file, I needed to get a tls.X509KeyPair from that. However, Go doesn’t have support for PFX. RavenDB makes extensive use of PFX in general, so that made things hard for us. I looked into all sorts of options, but I couldn’t find any way to manage that properly. The nearest find was the pkcs12 package, but that has support for only some DER format, and cannot handle common PFX files. That was a problem.
Luckily, I know how to use OpenSSL, but while there are countless examples on how to use OpenSSL to convert PFX to PEM and the other way around, all of them assume that you are using that from the command line, which isn’t what we want. It took me a bit of time, but I cobbled together a one off code that does the work. The code has a strange shape, I’m aware, because I wrote it to interface with Go, but it does the job.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
#include <string.h> #include <openssl/pkcs12.h> #include <openssl/pem.h> #include <openssl/bio.h> #include <openssl/err.h> #include <openssl/x509.h> void init_errors(){ ERR_load_crypto_strings(); } int get_pem_size(void * pem) { char * buf; int len = BIO_get_mem_data(pem, & buf); return len; } void copy_pem_to(void * pem, void * dst, int size) { char * buf; int len = BIO_get_mem_data(pem, & buf); memcpy(dst, buf, len > size ? size : len); } void free_pem(void * pem) { BIO_free(pem); } char * pfx_to_pem(void * data, long size, char * pwd, void ** key, void ** crt) { char * rc = NULL; BIO * bio = NULL; PKCS12 * p12 = NULL; EVP_PKEY * pkey = NULL; X509 * cert = NULL; STACK_OF(X509) * ca = NULL; BIO * key_bio = NULL; BIO * crt_bio = NULL; bio = BIO_new_mem_buf(data, size); if (!bio) { rc = "Unable to allocate memory buffer"; goto cleanup; } p12 = d2i_PKCS12_bio(bio, NULL); if (!p12) { rc = "Unable to read certificate"; goto cleanup; } if (!PKCS12_parse(p12, pwd, & pkey, & cert, & ca)) { rc = "Unable to parse certificate"; goto cleanup; } key_bio = BIO_new(BIO_s_mem()); if (!key_bio) { rc = "Out of memory, cannot create mem BIO for key"; goto cleanup; } if (!PEM_write_bio_PrivateKey(key_bio, pkey, NULL, NULL, 0, NULL, NULL)) { rc = "Failed to write PEM key output"; goto cleanup; } * key = key_bio; crt_bio = BIO_new(BIO_s_mem()); if (!crt_bio) { rc = "Out of memory, cannot create mem BIO for cert"; goto cleanup; } if (!PEM_write_bio_X509(crt_bio, cert)) { rc = "Failed to write PEM crt output"; goto cleanup; } * crt = crt_bio; goto success; cleanup: if (key_bio) BIO_free(key_bio); if (crt_bio) BIO_free(crt_bio); success: if (bio) BIO_free(bio); if (p12) PKCS12_free(p12); if (pkey) EVP_PKEY_free(pkey); if (cert) X509_free(cert); return rc; }
Now, from Go, I can run the following:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
//#cgo CFLAGS: "-IC:/Program Files/OpenSSL-Win64/include" //#cgo LDFLAGS: "-LC:/Program Files/OpenSSL-Win64/lib" -llibcrypto // #include "pfx.c" import "C" var initialized bool // from: // https://github.com/spacemonkeygo/openssl/blob/c2dcc5cca94ac8f7f3f0c20e20050d4cce9d9730/init.go func errorFromErrorQueue() string { if initialized == false { initialized = true C.init_errors() } var errs []string for { err := C.ERR_get_error() if err == 0 { break } errs = append(errs, fmt.Sprintf("%s:%s:%s", C.GoString(C.ERR_lib_error_string(err)), C.GoString(C.ERR_func_error_string(err)), C.GoString(C.ERR_reason_error_string(err)))) } return fmt.Sprintf("SSL errors: %s", strings.Join(errs, "\n")) } func pfx_to_pem(pfx []byte) (key_buf []byte, crt_buf []byte, err error) { var key *C.void var crt *C.void rc := C.pfx_to_pem(unsafe.Pointer(&pfx[0]), C.long(len(pfx)), nil, (*unsafe.Pointer)(unsafe.Pointer(&key)), (*unsafe.Pointer)(unsafe.Pointer(&crt))) if rc != nil { err = errors.New(C.GoString(rc) + "\n" + errorFromErrorQueue()) return } defer C.free_pem(unsafe.Pointer(key)) defer C.free_pem(unsafe.Pointer(crt)) size := C.get_pem_size(unsafe.Pointer(key)) key_buf = make([]byte, int(size)) C.copy_pem_to(unsafe.Pointer(key), unsafe.Pointer(&key_buf[0]), size) size = C.get_pem_size(unsafe.Pointer(key)) crt_buf = make([]byte, int(size)) C.copy_pem_to(unsafe.Pointer(crt), unsafe.Pointer(&crt_buf[0]), size) return }
As you can see, most of the code is there to manage error handling. But you can now convert a PFX to PEM and then pass that to X509keyPair easily.
That said, this seems just utterly ridiculous to me. There has got to be a better way to do that, surely.
Comments
Btw, both PFX and single PEM container file might contain more that one certificate (e.g. intermediate certificate and/or the private key) which I don't see how it's handled here.
Using something like
cat certificate.crt intermediates.pem private.key > ssl-certs.pem
and thenbind *:443 ssl crt ssl-certs.pem
is very common in haproxy setup for instance.wqw,
Yes, I'm making some assumptions about the kind of certificates that I'm getting here. If needed, the changes to support multiple items aren't that complex
Comment preview