require "openssl" key = OpenSSL::PKey::EC.generate("prime256v1") # P-256 under its OpenSSL name; "P-256" is rejected # EXCL refuses to overwrite an existing key; 0o600 keeps the file owner-onlyFile.open("key.pem", File::WRONLY | File::CREAT | File::EXCL, 0o600) do |file| file.write(key.private_to_pem) # "BEGIN PRIVATE KEY" (PKCS#8)endFile.write("pub.pem", key.public_to_pem) # "BEGIN PUBLIC KEY" (SPKI)puts key.public_to_pem # "BEGIN ENCRYPTED PRIVATE KEY": PKCS#8 under PBES2 with AES-256-CBCencrypted = key.private_to_pem(OpenSSL::Cipher.new("aes-256-cbc"), ENV.fetch("KEY_PASSPHRASE"))File.write("key-encrypted.pem", encrypted, perm: 0o600) How it works
OpenSSL::PKey::EC.generate("prime256v1")creates the key in one step. The olderEC.new(curve).generate_key!from many tutorials fails when Ruby is linked against OpenSSL 3:pkeys are immutable on OpenSSL 3.0.private_to_pemwrites PKCS#8 (BEGIN PRIVATE KEY) andpublic_to_pemwrites SPKI (BEGIN PUBLIC KEY); the run checks withopenssl pkeythat the two belong together.File::EXCLwith mode0o600creates the key file owner-only and fails if it already exists, so a second run cannot overwrite a key in use.private_to_pem(cipher, passphrase)encrypts the PKCS#8 structure (BEGIN ENCRYPTED PRIVATE KEY);OpenSSL::PKey.read(pem, passphrase)andopenssl pkey -passinboth open it.
Gotchas
- Curve names are OpenSSL's:
"prime256v1","secp384r1","secp521r1","secp256k1". Both"P-256"and"secp256r1"raiseOpenSSL::PKey::PKeyError: invalid curve name. key.to_pem(and its aliasexport) writesBEGIN EC PRIVATE KEY— the SEC 1 format that WebCrypto, Java and .NET's PKCS#8 importers reject. Useprivate_to_pemfor anything that leaves Ruby.- The
perm:option ofFile.writeapplies only when the file is created; an existing world-readable key file keeps its mode. TheFile::EXCLform above refuses to reuse a file at all. OpenSSL::PKey::EC#to_textand#private_keyexpose the private scalar; never log orinspectkey objects in production.