Skip to content
ecdsa.com

Self-signed certificate · Ruby

How to create a self-signed EC P-256 certificate with SANs in Ruby

Ruby's openssl library builds a certificate field by field: OpenSSL::X509::Certificate takes the version, the serial, subject and issuer, the public key and the validity, an ExtensionFactory creates the extensions from OpenSSL's config syntax, and sign signs with the P-256 key and SHA-256. The run decodes cert.pem with OpenSSL and verifies it for localhost and 127.0.0.1.

Tested with Ruby 4.0.7 (openssl gem 4.0.2, OpenSSL 3.5.7).

cert.rb — P-256 key and self-signed server certificate
require "openssl"
key = OpenSSL::PKey::EC.generate("prime256v1") # P-256
name = OpenSSL::X509::Name.parse("/CN=localhost")
cert = OpenSSL::X509::Certificate.new
cert.version = 2 # zero-based: 2 means X.509 v3
cert.serial = OpenSSL::BN.rand(127) # random, positive
cert.subject = name
cert.issuer = name # self-signed
cert.public_key = key
cert.not_before = Time.now
cert.not_after = cert.not_before + 365 * 24 * 60 * 60
extensions = OpenSSL::X509::ExtensionFactory.new(cert, cert) # subject and issuer certificate
cert.add_extension(extensions.create_extension("subjectAltName", "DNS:localhost,IP:127.0.0.1", false))
cert.add_extension(extensions.create_extension("basicConstraints", "CA:FALSE", true))
cert.add_extension(extensions.create_extension("keyUsage", "digitalSignature", true))
cert.add_extension(extensions.create_extension("extendedKeyUsage", "serverAuth", false))
cert.sign(key, OpenSSL::Digest.new("SHA256"))
File.write("cert.pem", cert.to_pem)
File.write("key.pem", key.private_to_pem)

How it works

  1. OpenSSL::PKey::EC.generate("prime256v1") creates the key; prime256v1 is OpenSSL's name for P-256.
  2. version = 2 is zero-based and means X.509 v3; serial = OpenSSL::BN.rand(127) is a random positive 127-bit number.
  3. Subject and issuer are the same X509::Name; not_after is not_before plus 365 days in seconds.
  4. create_extension(name, value, critical) takes the same strings as an openssl.cnf section, so "DNS:localhost,IP:127.0.0.1" is the SAN list; private_to_pem writes the key as PKCS#8.

Gotchas

  • key.to_pem writes the legacy SEC 1 block BEGIN EC PRIVATE KEY; private_to_pem writes the standard PKCS#8 BEGIN PRIVATE KEY.
  • OpenSSL::PKey::EC.new("prime256v1").generate_key!, common in older examples, fails on OpenSSL 3 with OpenSSL::PKey::PKeyError: pkeys are immutable on OpenSSL 3.0; use EC.generate.
  • An invalid SAN value fails only when the extension is created: "IP:localhost" raises OpenSSL::X509::ExtensionError: subjectAltName = IP:localhost: error in extension (name=subjectAltName, value=IP:localhost).

Related recipes