Skip to content
ecdsa.com

Generate a CSR · Ruby

How to generate a CSR for an EC P-256 key with SANs in Ruby

Ruby generates a CSR with the openssl default gem: OpenSSL::X509::Request holds the subject and public key, an extReq attribute carries the subjectAltName extension, and sign with a P-256 key and SHA-256 completes the request. The run decodes the CSR with OpenSSL.

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

csr.rb — P-256 key and CSR with two SANs
require "openssl"
key = OpenSSL::PKey::EC.generate("prime256v1") # P-256
csr = OpenSSL::X509::Request.new
csr.version = 0 # the only PKCS#10 version; printed as "Version: 1"
csr.subject = OpenSSL::X509::Name.parse("/C=US/O=Example Corp/CN=api.example.com")
csr.public_key = key
# Extensions travel in an extensionRequest attribute: a SET holding a SEQUENCE of extensions
san = OpenSSL::X509::ExtensionFactory.new.create_extension("subjectAltName", "DNS:api.example.com,DNS:www.example.com", false)
csr.add_attribute(OpenSSL::X509::Attribute.new("extReq", OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([san])])))
csr.sign(key, OpenSSL::Digest.new("SHA256"))
File.write("csr.pem", csr.to_pem)
File.write("key.pem", key.private_to_pem) # PKCS#8
puts "signature valid: #{csr.verify(key)}"

How it works

  1. OpenSSL::PKey::EC.generate("prime256v1") creates the P-256 key.
  2. OpenSSL::X509::Name.parse("/C=US/O=…/CN=…") builds the subject in the order written.
  3. ExtensionFactory#create_extension("subjectAltName", …) makes the extension; the extReq attribute holds a SET with one SEQUENCE of extensions.
  4. csr.sign(key, OpenSSL::Digest.new("SHA256")) signs the request; csr.verify(key) checks it.

Gotchas

  • The attribute value must be wrapped exactly: passing the SEQUENCE without the SET raises OpenSSL::X509::AttributeError: attribute value must be ASN1::Set.
  • There is no shortcut for SANs on X509::Request — unlike certificates, requests have no add_extension; the extReq attribute is the only way.
  • csr.version = 0 is the PKCS#10 version (OpenSSL prints it as Version: 1); the gem writes 0 even when it is not set, but setting it keeps the code explicit.
  • key.private_to_pem writes unencrypted PKCS#8; pass a cipher and passphrase (key.private_to_pem(OpenSSL::Cipher.new("aes-256-cbc"), pass)) to protect it.

Related recipes