require "openssl" key = OpenSSL::PKey::EC.generate("prime256v1") # P-256 csr = OpenSSL::X509::Request.newcsr.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 extensionssan = 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#8puts "signature valid: #{csr.verify(key)}"How it works
OpenSSL::PKey::EC.generate("prime256v1")creates the P-256 key.OpenSSL::X509::Name.parse("/C=US/O=…/CN=…")builds the subject in the order written.ExtensionFactory#create_extension("subjectAltName", …)makes the extension; theextReqattribute holds a SET with one SEQUENCE of extensions.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 noadd_extension; the extReq attribute is the only way. csr.version = 0is the PKCS#10 version (OpenSSL prints it asVersion: 1); the gem writes 0 even when it is not set, but setting it keeps the code explicit.key.private_to_pemwrites unencrypted PKCS#8; pass a cipher and passphrase (key.private_to_pem(OpenSSL::Cipher.new("aes-256-cbc"), pass)) to protect it.