# OpenSSL key generation: RSA, ECDSA, Ed25519 and ML-DSA

> Copy-paste OpenSSL 3.x commands to generate, encrypt, decrypt, inspect and match RSA, ECDSA, Ed25519, ML-DSA and ML-KEM private keys.

Source: https://ssl.sh/cookbook/generate-keys/
Last updated: 2026-11-10

In OpenSSL 3.x, `openssl genpkey` generates every private key type: RSA, ECDSA, Ed25519 and, from OpenSSL 3.5, the post-quantum ML-DSA and ML-KEM. The commands below also encrypt, inspect and match keys. Outputs come from OpenSSL 3.6.1; commands marked "3.5+" fail on older versions.

Check your version first with `openssl version`. On macOS the system `openssl` is LibreSSL, so use Homebrew's `$(brew --prefix openssl@3)/bin/openssl`.

## Generate an RSA private key

Use `genpkey` with `rsa_keygen_bits`; 2048 bits is the minimum public CAs accept and 3072 gives extra margin.

```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out example.com.key
# 3072 or 4096 bits
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out example.com.key
```

```text
..+.+.....+...+....+..+...+++++++++++++++++++++++++++++++*..+...+......
...+.+............+...+..+++++++++++++++++++++++++++++++*...........+++
```

The dots and plus signs are progress output from prime search and can be ignored; recent 3.x releases accept `-quiet` to hide them. The file starts with `-----BEGIN PRIVATE KEY-----` (PKCS#8). 4096-bit keys take noticeably longer to generate and make every handshake slower, with little practical benefit over 3072.

## Generate an RSA key with genrsa

`genrsa` still works and is what most older tutorials use; in 3.x it writes the same PKCS#8 format as `genpkey`.

```bash
openssl genrsa -out example.com.key 3072
head -1 example.com.key
```

```text
-----BEGIN PRIVATE KEY-----
```

Before OpenSSL 3.0, `genrsa` wrote `-----BEGIN RSA PRIVATE KEY-----` (PKCS#1). If a guide tells you to look for that header, the key is not broken; it uses the newer container. See "Convert a key to the traditional format" below if software insists on PKCS#1.

## Generate an ECDSA P-256 key

P-256 (OpenSSL name `prime256v1`) is the recommended default for TLS: a 256-bit key is roughly as strong as RSA 3072 and much faster to sign with.

```bash
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out example.com.key
# older equivalent, writes SEC1 "BEGIN EC PRIVATE KEY"
openssl ecparam -name prime256v1 -genkey -noout -out example.com.key
```

```text
-----BEGIN PRIVATE KEY-----
```

`genpkey` writes PKCS#8; `ecparam -genkey` writes the traditional SEC1 format (`BEGIN EC PRIVATE KEY`). Nginx, Apache, HAProxy and certbot accept both. Some old guides add `-param_enc explicit`; do not, because explicit curve parameters are rejected by many TLS stacks.

## Generate a P-384 key and list curves

Use P-384 when a policy (for example a government profile) requires it; otherwise P-256 is enough.

```bash
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out example.com.key
openssl ecparam -list_curves | grep -E "prime256v1|secp384r1|secp521r1"
```

```text
  secp384r1 : NIST/SECG curve over a 384 bit prime field
  secp521r1 : NIST/SECG curve over a 521 bit prime field
  prime256v1: X9.62/SECG curve over a 256 bit prime field
```

Public CAs issue TLS certificates for P-256, P-384 and P-521 keys only. `secp256k1` and Brainpool curves appear in the list but browsers do not accept them for TLS certificates.

## Generate an Ed25519 or Ed448 key

Ed25519 keys are tiny and fast, but they are only useful for private PKI, SSH-style tooling and mTLS inside your own systems.

```bash
openssl genpkey -algorithm ED25519 -out device.key
cat device.key
```

```text
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIPOx/N3NUcmxmTHOy4dbMZNVy+UQ0e5PhLTGx/frIr34
-----END PRIVATE KEY-----
```

Use `-algorithm ED448` for Ed448. As of November 2026 no publicly trusted CA issues Ed25519 server certificates, because the CA/Browser Forum Baseline Requirements allow only RSA and ECDSA. Ed25519 keys cannot take `-pkeyopt` options; there is nothing to choose.

## Encrypt a private key with a passphrase

Add a cipher option when you write the key; AES-256 is the usual choice.

```bash
# new key, encrypted
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
  -aes-256-cbc -out example.com.key
# encrypt an existing key
openssl pkey -in example.com.key -aes256 -out example.com.enc.key
head -1 example.com.enc.key
```

```text
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----BEGIN ENCRYPTED PRIVATE KEY-----
```

In scripts pass the password with `-pass pass:...` (genpkey) or `-passout file:secret.txt`; `pass:` puts the secret in your shell history and process list, so prefer `file:` or `env:VAR`. A web server cannot start unattended with an encrypted key unless you configure a passphrase helper (for example `ssl_password_file` in Nginx).

## Remove the passphrase from a key

Read the encrypted key and write it back without a cipher option.

```bash
openssl pkey -in example.com.enc.key -out example.com.key
```

```text
Enter pass phrase for example.com.enc.key:
```

If you see `Could not find private key ... UI_process:processing error`, OpenSSL could not ask for the password (for example in a non-interactive shell); pass it with `-passin file:secret.txt`. Set the unencrypted file to mode 600 and keep it out of Git.

## Extract the public key

The public key is derived from the private key; this is what a certificate or CSR will contain.

```bash
openssl pkey -in example.com.key -pubout -out example.com.pub
cat example.com.pub
```

```text
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3ESOpoc5gEeieqQ2KT82rP89m/8d
pKpjwWt960B2G9vF7f/s1cTOn3iPwpPxflR8Z6cmTwDqBHraRNtAD7D8Xg==
-----END PUBLIC KEY-----
```

The public key is safe to share. You need it for [public-key pinning](https://ssl.sh/glossary/certificate-pinning/), DANE [TLSA records](https://ssl.sh/tools/tlsa-generator/) and for proving which key a certificate was issued for.

## View and check a private key

`-text -noout` prints the key parameters without the PEM block; `-check` validates the math.

```bash
openssl pkey -in example.com.key -noout -text | head -12
openssl pkey -in example.com.key -noout -check
```

```text
Private-Key: (256 bit)
priv:
    71:90:82:72:65:87:d7:a5:c1:76:04:09:c2:8f:00:
    ...
pub:
    04:dc:44:8e:a6:87:39:80:47:a2:7a:a4:36:29:3f:
    ...
ASN1 OID: prime256v1
NIST CURVE: P-256
Key is valid
```

The first line tells you the algorithm and size: `Private-Key: (2048 bit, 2 primes)` for RSA, `ED25519 Private-Key:` for Ed25519. Never paste this output anywhere: the `priv` field is the secret itself.

## Check that a key matches a certificate or CSR

Compare a SHA-256 hash of the public key from each file; equal hashes mean they belong together.

```bash
openssl pkey -in example.com.key -pubout | openssl sha256
openssl x509 -in example.com.crt -noout -pubkey | openssl sha256
openssl req  -in example.com.csr -noout -pubkey | openssl sha256
```

```text
SHA2-256(stdin)= 1b4738615ecd9017a0dfbb8fe1d9c7654f80c8ee25cac47d06f9f818c9879dd6
SHA2-256(stdin)= 1b4738615ecd9017a0dfbb8fe1d9c7654f80c8ee25cac47d06f9f818c9879dd6
SHA2-256(stdin)= 1b4738615ecd9017a0dfbb8fe1d9c7654f80c8ee25cac47d06f9f818c9879dd6
```

The older `-modulus | openssl md5` trick only works for RSA; the public-key hash works for RSA, EC, Ed25519 and ML-DSA. A mismatch is what causes Nginx's [key values mismatch](https://ssl.sh/errors/nginx-key-values-mismatch/) error. The [key matcher tool](https://ssl.sh/tools/key-matcher/) does the same comparison in your browser.

## Convert a key to the traditional format

Some old Java, PHP and appliance software only reads `BEGIN RSA PRIVATE KEY` or `BEGIN EC PRIVATE KEY`; `-traditional` writes those.

```bash
openssl pkey -in example.com.key -traditional -out example.com.pkcs1.key
head -1 example.com.pkcs1.key
# and back to PKCS#8
openssl pkey -in example.com.pkcs1.key -out example.com.key
```

```text
-----BEGIN RSA PRIVATE KEY-----
```

PKCS#1 (RSA) and SEC1 (EC) are the "traditional" formats; PKCS#8 wraps any algorithm with an OID, which is why newer tools prefer it. Ed25519 and ML-DSA keys exist only in PKCS#8. The [format converter](https://ssl.sh/tools/ssl-converter/) and the [format conversion recipes](https://ssl.sh/cookbook/convert-formats/) cover DER and PFX as well.

## Generate an ML-DSA key (OpenSSL 3.5+)

ML-DSA (FIPS 204) is the NIST post-quantum signature algorithm; OpenSSL 3.5 and later can generate it with `genpkey`.

```bash
openssl genpkey -algorithm ML-DSA-65 -out pq.key
openssl pkey -in pq.key -noout -text | head -4
```

```text
ML-DSA-65 Private-Key:
seed:
    e0:ea:60:31:dd:5d:33:68:6b:ca:d0:c3:41:cb:3b:
    ...
```

Parameter sets are `ML-DSA-44`, `ML-DSA-65` and `ML-DSA-87`. The public key of ML-DSA-65 is 1,952 bytes and each signature is 3,309 bytes, so a self-signed ML-DSA-65 certificate made with these commands came out at 5,520 bytes of DER versus 468 bytes for P-256. Use these keys for lab work and private PKI; see [post-quantum TLS](https://ssl.sh/learn/post-quantum-tls/) for why public certificates are not there yet.

## Generate an ML-KEM key (OpenSSL 3.5+)

ML-KEM (FIPS 203) is a key-encapsulation mechanism, not a signature scheme, so it never goes into a TLS server certificate.

```bash
openssl genpkey -algorithm ML-KEM-768 -out kem.key
openssl list -kem-algorithms | grep -i mlkem
```

```text
  { 2.16.840.1.101.3.4.4.2, id-alg-ml-kem-768, ML-KEM-768, MLKEM768 } @ default
  X25519MLKEM768 @ default
  X448MLKEM1024 @ default
  SecP256r1MLKEM768 @ default
```

In TLS, ML-KEM is used for ephemeral key exchange: since 3.5, OpenSSL's default client keyshares include the hybrid group `X25519MLKEM768`, and you never create those keys by hand. A standalone ML-KEM key is useful for testing HPKE-style encryption or interoperability. Check what is available with `openssl list -signature-algorithms` and `openssl list -kem-algorithms`.
