Skip to content
ecdsa.com

Create a PKCS#12 bundle · Ruby

How to create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Ruby

To create a PKCS#12 (.p12/.pfx) file from a key and certificate chain in Ruby, call OpenSSL::PKCS12.create(password, name, key, certificate, chain) and write to_der; OpenSSL::PKCS12.new(der, password) reads it back. This recipe bundles a P-256 key with its certificate and issuing CA and checks the file with OpenSSL.

Tested with openssl (Ruby gem) 4.0.2 (Ruby 4.0.7, OpenSSL 3.5.7) and OpenSSL 3.5.7 (reads the bundle back (inspect.sh)).

create.rb — key + certificate + chain into bundle.p12
require "openssl"
password = ENV.fetch("P12_PASSWORD")
key = OpenSSL::PKey.read(File.read("key.pem"))
leaf = OpenSSL::X509::Certificate.new(File.read("leaf.pem"))
chain = [OpenSSL::X509::Certificate.new(File.read("intermediate.pem"))]
# name is the friendly name; the protection algorithms are OpenSSL's defaults (AES-256 with OpenSSL 3)
p12 = OpenSSL::PKCS12.create(password, "api.example.com", key, leaf, chain)
File.binwrite("bundle.p12", p12.to_der)
read.rb — read the bundle back: certificate, chain, key
require "openssl"
password = ENV.fetch("P12_PASSWORD")
p12 = OpenSSL::PKCS12.new(File.binread("bundle.p12"), password)
puts "certificate: #{p12.certificate.subject.to_s(OpenSSL::X509::Name::RFC2253)}"
p12.ca_certs.to_a.each { |cert| puts "chain: #{cert.subject.to_s(OpenSSL::X509::Name::RFC2253)}" }
puts "key matches certificate: #{p12.certificate.check_private_key(p12.key)}"
begin
OpenSSL::PKCS12.new(File.binread("bundle.p12"), "wrong-password")
rescue OpenSSL::PKCS12::PKCS12Error => e
puts "wrong password: #{e.message}"
end

How it works

  1. OpenSSL::PKCS12.create(password, name, key, cert, ca_certs) builds the bundle; name becomes the friendly name and ca_certs is the chain without the leaf.
  2. Without explicit algorithm arguments the library uses OpenSSL's defaults: AES-256-CBC with PBKDF2-HMAC-SHA256 and an HMAC-SHA-256 MAC on OpenSSL 3.5, as the run shows.
  3. OpenSSL::PKCS12.new(der, password) exposes key, certificate and ca_certs; check_private_key confirms the pair.
  4. Write and read the file with File.binwrite / File.binread: it is binary DER.

Gotchas

  • A wrong password raises OpenSSL::PKCS12::PKCS12Error: PKCS12_parse: mac verify failure; rescue it explicitly.
  • ca_certs is nil, not an empty array, when the bundle has no chain (checked in the run) — iterate over ca_certs.to_a.
  • The protection is whatever the linked OpenSSL uses by default (AES-256 with OpenSSL 3.5 here); pass the algorithm arguments of create when the output must not depend on the system Ruby runs on.

Related recipes