Skip to content
ecdsa.com

Export public key PEM · Ruby

How to export a public key as PEM in Ruby

To export a public key as PEM in Ruby from an ECDSA private key, read the private key with OpenSSL::PKey.read and call public_to_pem — one method that writes the SPKI BEGIN PUBLIC KEY block, byte-identical to openssl pkey -pubout. The same key object also yields the uncompressed point for a JWK.

Tested with openssl (Ruby gem) 4.0.2 (Ruby 4.0.7, OpenSSL 3.5.7) and OpenSSL 3.5.7 (openssl pkey cross-check).

export.rb — private key PEM (PKCS#8 or SEC 1) in, SPKI PEM out
require "openssl"
# PKCS#8 or SEC 1; "" instead of nil so an encrypted key fails instead of prompting
private_key = OpenSSL::PKey.read(File.read(ARGV.fetch(0, "key.pem")), ENV.fetch("KEY_PASSPHRASE", ""))
public_pem = private_key.public_to_pem # SPKI, "BEGIN PUBLIC KEY"
File.write("pub.pem", public_pem)
puts public_pem
the same public key as a JWK
# The same public key as a JWK: the uncompressed point 0x04 ‖ x ‖ y, split and base64url-encoded
point = OpenSSL::PKey.read(File.read("pub.pem")).public_key.to_octet_string(:uncompressed)
x, y = point.byteslice(1, 32), point.byteslice(33, 32) # fixed width, leading zeros kept
b64url = ->(bytes) { [bytes].pack("m0").tr("+/", "-_").delete("=") }
puts({ kty: "EC", crv: "P-256", x: b64url.(x), y: b64url.(y) }.to_json)

How it works

  1. OpenSSL::PKey.read loads BEGIN PRIVATE KEY and BEGIN EC PRIVATE KEY files alike; the run checks that both give the same public PEM.
  2. public_to_pem writes the public half only, as SPKI with the curve OID; public_to_der gives the same structure as bytes.
  3. public_key.to_octet_string(:uncompressed) returns the 65-byte point 0x04 ‖ x ‖ y with both coordinates at full width, so slicing it gives correct JWK coordinates.
  4. pack("m0") followed by tr and delete("=") is base64url without padding, the encoding JWK members use.

Gotchas

  • to_pem on a private key exports the private key (as SEC 1), not the public one. Use public_to_pem whenever the output is meant to be shared.
  • Going through public_key.to_bn.to_s(2) or OpenSSL::BN#to_s(2) for single coordinates drops leading zero bytes; slicing the octet string, as above, keeps them.
  • OpenSSL::PKey.read(pem) without a second argument prompts on the terminal for an encrypted key. Pass the passphrase, or "" to fail fast.
  • A public key loaded from an environment variable often has literal \n sequences instead of newlines and fails with Could not parse PKey; restore the line breaks first.

Related recipes