OpenSSL 3.5 makes a post-quantum ML-DSA key with the same genpkey command you use for RSA. And when you test a big site with s_client, the key exchange it reports is already the hybrid X25519MLKEM768, not plain X25519. Most OpenSSL cheat sheets still teach -nodes and genrsa, and none of the ones we checked show either of those things.
This cheat sheet covers the OpenSSL commands admins actually type: keys, CSRs, self-signed and CA-signed certificates, inspection, format conversion, TLS testing, post-quantum algorithms and file encryption. Every command in the code blocks was run in October 2026 on Ubuntu 26.04 (OpenSSL 3.5.5) and Rocky Linux 10.2 (OpenSSL 3.5.8), and we note each place the two distros behave differently, mostly because of Rocky’s crypto-policies. The exact errors and their fixes are at the end, and the whole sheet is a one-page PDF.
Find the OpenSSL command for the job
Use this table when you know what you need but not the syntax. Each row is tested further down, with output from both distros:
| Job | Command |
|---|---|
| Make an EC private key | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec.key |
| Make a post-quantum key | openssl genpkey -algorithm ML-DSA-65 -out mldsa.key |
| CSR with SANs | openssl req -new -key ec.key -subj "/CN=web01.example.com" -addext "subjectAltName=DNS:web01.example.com" -out web01.csr |
| Self-signed cert, key unencrypted | openssl req -x509 -newkey rsa:2048 -noenc -keyout self.key -out self.crt -days 365 -subj "/CN=lab.example.com" |
| Read a certificate | openssl x509 -in self.crt -noout -subject -issuer -dates -ext subjectAltName |
| Will it expire within a day? | openssl x509 -in self.crt -noout -checkend 86400 |
| Does this key match this cert? | Compare openssl x509 -in self.crt -noout -pubkey | openssl sha256 with openssl pkey -in self.key -pubout | openssl sha256 |
| PEM to PKCS#12 | openssl pkcs12 -export -in self.crt -inkey self.key -out self.p12 |
| Test a TLS server | openssl s_client -connect example.com:443 -brief </dev/null |
| Expiry of a live site | echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate |
| Verify a chain | openssl verify -CAfile ca.crt web01.crt |
| Encrypt a file | openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt -in app.env -out app.env.enc |
| Random secret | openssl rand -base64 32 |
Check your OpenSSL version and providers
Almost every behaviour difference comes down to the version and the providers that are loaded, so check both first:
openssl version; openssl list -providers
Ubuntu 26.04 printed this, with only the default provider active, which is normal. Rocky Linux 10.2 printed the same apart from the 3.5.8 version string:
OpenSSL 3.5.5 27 Jan 2026 (Library: OpenSSL 3.5.5 27 Jan 2026)
Providers:
default
name: OpenSSL Default Provider
version: 3.5.5
status: active
Each distro ships the 3.5 long-term support line, which upstream supports until April 2030, and neither has moved to 4.0. Ubuntu’s 3.5.5 carries backported security fixes, so don’t compare its number with upstream’s latest 3.5 release. The legacy provider (old ciphers such as RC2, DES and MD4) is installed on both but not loaded, and that one fact explains the most common OpenSSL 3 error, covered below. On Ubuntu it lives in its own openssl-provider-legacy package, which the main library depends on, so apt always installs it.
Generate private keys
genpkey is the one command for every key type, so you don’t need to remember genrsa, ecparam and friends. We generated one of each kind on both distros:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out rsa.key
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec.key
openssl genpkey -algorithm ED25519 -out ed.key
openssl genpkey -algorithm ML-DSA-65 -out mldsa.key
openssl genpkey -algorithm ML-KEM-768 -out mlkem.key
openssl genpkey -algorithm SLH-DSA-SHA2-128s -out slh.key
The file sizes say a lot about the post-quantum trade-off. All keys came out with mode 0600 because genpkey creates them that way:
| Key | Use | File size (bytes) |
|---|---|---|
| Ed25519 | Signatures | 119 |
| SLH-DSA-SHA2-128s | Post-quantum signatures, hash based | 168 |
| EC P-256 | Signatures, ECDH | 241 |
| RSA 3072 | Signatures, encryption | 2,484 (2,488 on Rocky) |
| ML-KEM-768 | Post-quantum key exchange only | 3,439 |
| ML-DSA-65 | Post-quantum signatures | 5,604 |
RSA itself prints a line of dots and plus signs while it searches for primes, which is normal. The older genrsa still works, but since OpenSSL 3.0 it writes the same PKCS#8 format as genpkey. Our openssl genrsa -out g.key 2048 produced a file starting -----BEGIN PRIVATE KEY-----. If an old tool insists on BEGIN RSA PRIVATE KEY, convert it:
openssl pkey -in g.key -traditional -out g-rsa.key
To protect a key with a passphrase, or take one off, use pkey as well. Encryption writes BEGIN ENCRYPTED PRIVATE KEY, and a wrong passphrase fails with bad decrypt:
openssl pkey -in ec.key -aes256 -out ec-enc.key
openssl pkey -in ec-enc.key -out ec-plain.key
Read a key’s details with openssl pkey -in ec.key -text -noout, which starts with Private-Key: (256 bit) for P-256. Keep the -text output off shared screens, because it prints the private key material in hex.
Create a CSR with subject alternative names
Browsers and most clients ignore the CN and check only the SAN list, so a CSR without SANs is useless for TLS. -addext adds them on the command line, with no config file to edit:
openssl req -new -key ec.key \
-subj "/CN=web01.example.com/O=Example" \
-addext "subjectAltName=DNS:web01.example.com,DNS:www.example.com,IP:10.0.1.50" \
-out web01.csr
Check the request’s own signature and the SANs before you send it anywhere:
openssl req -in web01.csr -noout -subject -verify
openssl req -in web01.csr -noout -text | grep -A1 'Subject Alternative Name'
Both distros confirmed the signature and listed all three names:
Certificate request self-signature verify OK
subject=CN=web01.example.com, O=Example
X509v3 Subject Alternative Name:
DNS:web01.example.com, DNS:www.example.com, IP Address:10.0.1.50
Public CAs such as Let’s Encrypt only accept DNS names, and they read only the SAN list, so get it right here. Our Let’s Encrypt guide covers the automated route when you don’t need a hand-made CSR.
Make a self-signed certificate
One req -x509 command makes the key and the certificate together. Use -noenc to leave the key unencrypted, which is what a service that starts unattended needs:
openssl req -x509 -newkey rsa:2048 -noenc -keyout self.key -out self.crt -days 365 \
-subj "/CN=lab.example.com" -addext "subjectAltName=DNS:lab.example.com"
openssl x509 -in self.crt -noout -subject -issuer -dates -ext subjectAltName
Subject and issuer match, which is what makes it self-signed:
subject=CN=lab.example.com
issuer=CN=lab.example.com
notBefore=Oct 22 01:04:15 2026 GMT
notAfter=Oct 22 01:04:15 2027 GMT
X509v3 Subject Alternative Name:
DNS:lab.example.com
-noenc replaced -nodes in OpenSSL 3.0. The old flag is deprecated but still accepted, and it printed no warning on either distro, so old scripts keep working without telling you. Two other details are worth knowing:
- the certificate is marked
CA:TRUEby default, because the defaultopenssl.cnfapplies itsv3_casection toreq -x509 -not_before 20261101000000Z -not_after 20271101000000Zsets exact dates instead of-days
The first one matters if you give the certificate to a strict client as a server certificate. Add -addext "basicConstraints=critical,CA:FALSE" for a plain leaf. For a certificate your browsers trust without warnings, mkcert installs a local CA for you, and we have an Ansible version of the self-signed workflow for fleets.
Sign certificates with your own CA
A two-file CA is enough for a lab, internal services or mutual TLS tests. Create the CA with the right extensions, then sign the CSR from earlier. -copy_extensions copy carries the SANs from the CSR into the certificate; without it they are dropped:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc -keyout ca.key -out ca.crt -days 3650 \
-subj "/CN=Example Lab Root CA" \
-addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl x509 -req -in web01.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 397 \
-copy_extensions copy -out web01.crt
The signing step checks the CSR first, then the new certificate shows our CA as issuer and keeps all three SANs:
Certificate request self-signature ok
subject=CN=web01.example.com, O=Example
subject=CN=web01.example.com, O=Example
issuer=CN=Example Lab Root CA
X509v3 Subject Alternative Name:
DNS:web01.example.com, DNS:www.example.com, IP Address:10.0.1.50
Verify the chain against the CA, and see what happens without it:
openssl verify -CAfile ca.crt web01.crt
openssl verify web01.crt
The first line passes; the second fails because our CA is not in the system trust store:
web01.crt: OK
CN=web01.example.com, O=Example
error 20 at 0 depth lookup: unable to get local issuer certificate
error web01.crt: verification failed
To make every tool on the box trust the CA, which we did and then undid on both distros, copy ca.crt into /usr/local/share/ca-certificates/ and run sudo update-ca-certificates on Ubuntu, or into /etc/pki/ca-trust/source/anchors/ and run sudo update-ca-trust on Rocky Linux. After that, a plain openssl verify web01.crt printed web01.crt: OK with no -CAfile. Be careful with this on a shared box: ca.key was made with -noenc, so anyone who can read it can mint certificates the whole system now trusts. Move it somewhere safe or encrypt it. To undo the trust, delete the file you copied and run sudo update-ca-certificates --fresh on Ubuntu or sudo update-ca-trust on Rocky; openssl verify web01.crt failed again for us afterwards. The bundles they build are /etc/ssl/certs/ca-certificates.crt on Ubuntu and /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem on Rocky, where the old /etc/pki/tls/certs/ca-bundle.crt is now a symlink to it.
Inspect certificates, CSRs and keys
x509 prints any field on its own, which beats scrolling through -text. These are the flags we use most:
| Command | Shows |
|---|---|
openssl x509 -in c.crt -noout -text | Everything |
-subject -issuer | Who it is for and who signed it |
-dates / -enddate | Validity window |
-ext subjectAltName,basicConstraints | Only the named extensions |
-fingerprint -sha256 | Fingerprint to compare with a browser or pin |
-checkend 86400 | Exit 1 if it expires within the given seconds |
-serial | Serial number, for revocation |
openssl req -in r.csr -noout -text -verify | A CSR and its signature check |
-checkend is the one to put in monitoring, because the exit code does the work. Our new certificate was good for a day but not for a full year:
openssl x509 -in self.crt -noout -checkend 86400; echo "checkend 1d exit=$?"
openssl x509 -in self.crt -noout -checkend 31536000; echo "checkend 365d exit=$?"
The text and the exit code agree:
Certificate will not expire
checkend 1d exit=0
Certificate will expire
checkend 365d exit=1
Matching a key to a certificate is the other daily job, usually after a renewal leaves several keys in one directory. The common -modulus trick only works for RSA, so compare a hash of the public key instead, which works for every key type:
openssl x509 -in self.crt -noout -pubkey | openssl sha256
openssl pkey -in self.key -pubout | openssl sha256
openssl pkey -in ec.key -pubout | openssl sha256
The certificate and its own key give the same hash; the unrelated EC key does not:
SHA2-256(stdin)= 8a3b597f011c9eb72cd17064b7f286481e46ee636d8d4e7a09db31461febe37e
SHA2-256(stdin)= 8a3b597f011c9eb72cd17064b7f286481e46ee636d8d4e7a09db31461febe37e
SHA2-256(stdin)= cd0ef2fe883d279dc85c57864c93912450fa8a2f8bb967132a3a8fe7db726356
For a deeper look at expiry checks, including bulk checks across many hosts, see our guide to checking certificate expiration with OpenSSL.
Convert between PEM, DER, PKCS#12 and PKCS#7
Windows, Java and network appliances each want a different container, and these commands cover the usual requests:
| From | To | Command |
|---|---|---|
| PEM cert | DER | openssl x509 -in c.crt -outform DER -out c.der |
| DER cert | PEM | openssl x509 -in c.der -inform DER -out c.pem |
| PEM cert + key | PKCS#12 (.p12/.pfx) | openssl pkcs12 -export -in c.crt -inkey c.key -out c.p12 |
| PKCS#12 | PEM cert | openssl pkcs12 -in c.p12 -nokeys -out c.crt |
| PKCS#12 | PEM key, unencrypted | openssl pkcs12 -in c.p12 -nocerts -noenc -out c.key |
| PEM cert | PKCS#7 (.p7b) | openssl crl2pkcs7 -nocrl -certfile c.crt -out c.p7b |
| PKCS#7 | PEM certs | openssl pkcs7 -in c.p7b -print_certs -out certs.pem |
| PKCS#8 key | Traditional RSA key | openssl pkey -in k.key -traditional -out k-rsa.key |
The 3.5 tools also detect DER on input, so openssl x509 -in self.der -noout worked on both distros without -inform DER. Older guides still say it fails with Expecting: TRUSTED CERTIFICATE, and on 3.5 it doesn’t. PKCS#12 is where the 3.x change bites. A new .p12 uses AES-256 and a SHA-256 MAC, which pkcs12 -info shows plainly:
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 8
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Certificate bag
PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Some older consumers can’t read that format: older Java builds, older macOS Keychain releases, some appliances. For those, export with -legacy, which uses a SHA-1 MAC, 40-bit RC2 for the certificate and 3DES for the key. The flip side is that OpenSSL 3 cannot read such a file without the same flag, and that produces the 0308010C error in the errors section.
openssl pkcs12 -export -legacy -in self.crt -inkey self.key -out legacy.p12
The legacy file reports MAC: sha1 and pbeWithSHA1And40BitRC2-CBC, which shows how weak it is. Send it only to the device that needs it.
Test TLS servers with s_client
s_client is the fastest way to answer what a server negotiates and what certificate it sends. -brief cuts the output to a summary, and </dev/null closes the connection instead of waiting for input:
openssl s_client -connect www.google.com:443 -servername www.google.com -brief </dev/null
On both distros, the default handshake with Google used the hybrid post-quantum group. Rocky printed the same lines as Ubuntu:
Connecting to 142.251.153.119
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.google.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE
We passed -servername there out of habit, but SNI is sent automatically for a hostname; our runs without it negotiated the same group. -servername is only needed when you connect to an IP address or want a different name than the one you dial. The rest of the useful flags:
| Flag | Use |
|---|---|
-showcerts | Print every certificate the server sends (3 for Google); this is not a verified chain |
-tls1_2 / -tls1_3 | Force a version |
-groups x25519 | Force a key exchange group |
-verify_hostname name | Check the certificate against a name |
-CAfile ca.crt | Trust a private CA for this test |
-starttls smtp | Upgrade a plain SMTP, IMAP, POP3 or FTP session first |
Forcing a classic group shows the older label. With -groups x25519 the TLS 1.3 handshake printed Peer Temp Key: X25519, 253 bits instead of the negotiated group line, and the same label appears for TLS 1.2. Readers on OpenSSL 3.0, such as Ubuntu 24.04, see Server Temp Key in its place. Two one-liners cover most expiry and chain questions:
echo | openssl s_client -connect www.google.com:443 -servername www.google.com 2>/dev/null | openssl x509 -noout -subject -issuer -enddate
openssl s_client -connect www.google.com:443 -verify_hostname wrong.example.com -brief </dev/null
The first printed the subject, issuer=C=US, O=Google Trust Services, CN=WE2 and the expiry date. The second failed the name check with verify error:num=62:hostname mismatch, the same check a client makes when the SANs are wrong.
Use post-quantum algorithms
OpenSSL 3.5 added the three NIST post-quantum standards to the default provider: ML-KEM for key exchange, ML-DSA and SLH-DSA for signatures. List what your build has:
openssl list -signature-algorithms | grep -iE 'ml-dsa|slh-dsa'
openssl list -kem-algorithms | grep -iE 'ml-kem|mlkem'
The KEM list adds the hybrid KEMs to plain ML-KEM (excerpt). openssl list -tls1_3 -tls-groups shows which of them TLS can use: the pure MLKEM512, MLKEM768 and MLKEM1024, plus three of the four hybrids, X25519MLKEM768, SecP256r1MLKEM768 and SecP384r1MLKEM1024. X448MLKEM1024 is a KEM you can’t pass to -groups. Being a TLS group doesn’t mean a server accepts it, either: -groups MLKEM768 against Google ended in ssl/tls alert handshake failure on both distros, because Google only offers the hybrid:
{ 2.16.840.1.101.3.4.4.1, id-alg-ml-kem-512, ML-KEM-512, MLKEM512 } @ default
{ 2.16.840.1.101.3.4.4.2, id-alg-ml-kem-768, ML-KEM-768, MLKEM768 } @ default
{ 2.16.840.1.101.3.4.4.3, id-alg-ml-kem-1024, ML-KEM-1024, MLKEM1024 } @ default
X25519MLKEM768 @ default
X448MLKEM1024 @ default
SecP256r1MLKEM768 @ default
SecP384r1MLKEM1024 @ default
ML-DSA keys sign certificates like any other key. We made a self-signed ML-DSA certificate, served it with s_server and connected to it, so both the signature and the key exchange were post-quantum:
openssl req -x509 -key mldsa.key -out mldsa.crt -days 30 -subj "/CN=pq.example.com"
openssl s_server -accept 4433 -cert mldsa.crt -key mldsa.key -www >/dev/null 2>&1 &
sleep 1
openssl s_client -connect 127.0.0.1:4433 -CAfile mldsa.crt -brief </dev/null
kill $!
The sleep gives the server a moment to start listening, and kill $! stops exactly the server you started. The handshake worked on both distros (filtered to the relevant lines):
Protocol version: TLSv1.3
Signature type: mldsa65
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
The cost shows up in size, not speed. The ML-DSA certificate was 7,529 bytes against 1,168 for RSA 2048, so every handshake carries several kilobytes more. Signing speed falls between RSA 2048 and RSA 3072, key generation is hundreds of times faster, and verification is the one area where RSA stays well ahead. We ran openssl speed -seconds 1 -signature-algorithms and -kem-algorithms on the same Ubuntu VM:
| Algorithm | Keygen/s | Sign/s or encaps/s | Verify/s or decaps/s |
|---|---|---|---|
| RSA 2048 | 17.8 | 1,813 | 62,608 |
| RSA 3072 | 5.9 | 593 | 28,736 |
| ML-DSA-44 | 8,063.6 | 1,416 | 7,126 |
| ML-DSA-65 | 4,427 | 897 | 4,665.7 |
| X25519 (KEM) | 24,040 | 12,017.2 | 26,583 |
| ML-KEM-768 | 18,359.6 | 29,500 | 19,522 |
| X25519MLKEM768 | 10,194 | 9,173.7 | 11,560 |
Those figures are from the Ubuntu VM. Rocky’s VM ran on a different physical host and its numbers moved in both directions; there ML-DSA-65 signed at 1,187 per second against 1,361 for RSA 3072, so treat the comparison as a rough guide, not a ranking. Only speed options run these: openssl speed ml-dsa-65 answered Unknown algorithm, so use the -signature-algorithms or -kem-algorithms switch. Both run every algorithm in the class, so expect a few minutes.
Know what Rocky Linux crypto-policies block
On Rocky Linux and RHEL, a system-wide crypto policy sits on top of OpenSSL’s own defaults, and it is the main reason the same command can work on Ubuntu and fail on Rocky. Check the active one with update-crypto-policies --show; ours was DEFAULT. We switched between policies on the lab VM and ran the same commands each time:
| Test | Ubuntu 26.04 | Rocky DEFAULT | Rocky LEGACY | Rocky FUTURE |
|---|---|---|---|---|
Sign a cert with -sha1 | Works | invalid digest | Works | Not tested |
| Serve a 1024-bit RSA cert | ee key too small | ee key too small | Not tested | Not tested |
| Serve a 2048-bit RSA cert | Works | Works | Not tested | ee key too small |
| Group with www.google.com | X25519MLKEM768 | X25519MLKEM768 | Not tested | X25519MLKEM768 |
Force -tls1_1 | no protocols available | no protocols available | Not tested | Not tested |
Ubuntu has no crypto-policy layer by default, and its OpenSSL already runs at security level 2, which is why it also refuses 1024-bit keys and TLS 1.1. FUTURE raised the minimum RSA size to 3072 bits, so our 2048-bit lab certificate failed. It still reached Google, because Google offers the hybrid group FUTURE requires. Post-quantum key exchange is on in every predefined Rocky policy. To turn it off for one system, for instance when a middlebox chokes on the larger handshake, use the NO-PQ subpolicy:
sudo update-crypto-policies --set DEFAULT:NO-PQ
openssl s_client -connect www.google.com:443 -brief </dev/null 2>&1 | grep -E 'group|Temp Key'
sudo update-crypto-policies --set DEFAULT
Under DEFAULT:NO-PQ the handshake fell back to Peer Temp Key: X25519, 253 bits. Policy changes apply to new processes, so restart services (or reboot) after switching, and switch back to DEFAULT when you finish testing. If you need SHA-1 signatures for an old device, LEGACY re-enabled them for us, but it does not bring back TLS 1.0 or 1.1. Changing the policy for one old certificate weakens the whole box, so reissuing with SHA-256 is the better fix.
Hash, encode and encrypt files
OpenSSL doubles as a general crypto toolbox. These worked the same on both distros:
| Job | Command |
|---|---|
| SHA-256 of a file | openssl dgst -sha256 file |
| Hash stdin | printf 'text' | openssl sha256 |
| Base64 encode | printf 'hello' | openssl base64 |
| Base64 decode | printf 'aGVsbG8=\n' | openssl base64 -d |
| Random hex (16 bytes) | openssl rand -hex 16 |
| Random base64 (32 bytes) | openssl rand -base64 32 |
| Encrypt a file | openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt -in f -out f.enc |
| Decrypt it | openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in f.enc -out f |
Base64 decoding has a trap. openssl base64 -d expects a newline at the end of its input, so printf 'aGVsbG8=' | openssl base64 -d printed nothing at all, zero bytes, on both distros. Add the newline, or pass -A to read the input as one line, and you get hello back.
For enc, always pass -pbkdf2 and an iteration count. Without them OpenSSL derives the key the old, fast way and warns you about it:
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
Decryption has to use the same -pbkdf2 -iter values, or it fails with bad decrypt. For secrets you share with people, age or GPG are friendlier, but enc is on every server already. If you want one book that explains the protocols behind all of these commands, Ivan Ristić’s Bulletproof TLS and PKI (paid link) (second edition, Feisty Duck) is the one we keep coming back to. It predates the post-quantum additions, so pair it with this page for those, but its chapters on PKI, certificate validation and server configuration are as useful as ever.
Fix common OpenSSL errors
These are the errors we triggered in the lab, with the text OpenSSL 3.5 printed. The long hex prefix on each error: line is a thread ID and changes every run, so search for the part after it.
Error: “error:0308010C:digital envelope routines::unsupported”
OpenSSL 3 cannot find an algorithm because it lives in the legacy provider, which is not loaded. Reading an old .pfx is the usual trigger:
Error outputting keys and certificates
40A72774F07D0000:error:0308010C:digital envelope routines:inner_evp_generic_fetch:unsupported:../crypto/evp/evp_fetch.c:376:Global default library context, Algorithm (RC2-40-CBC : 0), Properties ()
Add -legacy to the pkcs12 command, or load both providers with -provider legacy -provider default; both read the file on both distros. Then re-export it without -legacy so the next person doesn’t hit the same wall. Node.js builds print the same code for an MD4 hash, and the cause there is the same.
Error: “Mac verify error: invalid password?”
The .p12 password is wrong. Our test with a wrong -passin printed exactly this. Check the password first. An old file with the right password gets past this check, because its SHA-1 MAC is still supported, and then fails with the 0308010C error above instead; that one needs -legacy.
Error: “unable to get local issuer certificate” (verify error 20)
OpenSSL can’t find the certificate that signed this one. For a private CA, pass it with -CAfile or add it to the system store as shown in the CA section. For a public site, the server is usually not sending its intermediate certificate; -showcerts shows what it sends, and the fix is to configure the full chain on the server.
Error: “self-signed certificate” (verify error 18)
openssl verify self.crt printed error 18 at 0 depth lookup: self-signed certificate. That is expected for a self-signed certificate nobody trusts yet. Trust it explicitly with -CAfile self.crt for a test, or sign real services from your own CA so clients trust one root instead of many leaves. Error 19, self-signed certificate in certificate chain, is the same problem one level up.
Error: “verify error:num=62:hostname mismatch”
The certificate is valid but not for the name you connected to. Check the SAN list with openssl x509 -noout -ext subjectAltName and reissue with the missing name in -addext subjectAltName. The CN is not checked when SANs are present.
Error: “SSL_CTX_use_certificate:ee key too small”
s_server refused to load a 1024-bit RSA certificate on both distros, and Rocky’s FUTURE policy refused a 2048-bit one with the same error. The key is below the security level: 2048 bits at the default level 2, 3072 under FUTURE. Reissue with a larger RSA key or switch to EC. On the verifying side, the same problem reads EE certificate key too weak.
Error: “do_sigver_init:invalid digest” on Rocky Linux
Rocky’s DEFAULT crypto policy forbids SHA-1 signatures, so openssl req -x509 -sha1 failed with error:03000098:digital envelope routines:do_sigver_init:invalid digest and exit 1. The same command worked on Ubuntu. Drop -sha1 and let OpenSSL pick SHA-256. Only switch to the LEGACY policy if a device you can’t replace insists on SHA-1.
Error: “no protocols available”
You forced a protocol version the security level forbids. -tls1_1 printed tls_setup_handshake:no protocols available on both distros after the TCP connection opened but before any TLS record was sent. That is OpenSSL refusing on your side, not the server. If you really need to test a TLS 1.1 server, only a throwaway client with -cipher DEFAULT@SECLEVEL=0 will try.
Error: “wrong version number” or “packet length too long”
You spoke TLS to a port that answers in plain text. We pointed s_client at nginx on port 80, and the two distros reported it differently: Rocky printed tls_validate_record_header:wrong version number and Ubuntu printed tls_get_more_records:packet length too long. Both mean the same thing. Use the TLS port, or -starttls smtp (or imap, pop3, ftp) for services that upgrade a plain connection.
Error: “Could not find certificate from file”
The file is not a certificate. We got Could not find certificate from web01.csr for a CSR, and the same for a private key, each followed by a STORE routines unsupported line. A file of junk printed the first line alone. Check the first line with head -1, because the header tells you which command the file needs:
BEGIN CERTIFICATE:openssl x509BEGIN CERTIFICATE REQUEST:openssl reqBEGIN PRIVATE KEYorBEGIN ENCRYPTED PRIVATE KEY:openssl pkey- no header at all: DER, which 3.5 reads directly
A missing file reads Could not open file or uri for loading certificate from nonexistent.pem: No such file or directory instead.
Error: “key values mismatch”
The certificate and key don’t belong together. Signing with the wrong CA key printed CA certificate and CA private key do not match followed by this line. Other commands word the same problem differently: pkcs12 -export printed No cert in -in file 'web01.crt' matches private key, and s_server printed Private key does not match the certificate public key. Compare the public key hashes as shown in the inspection section to find the right pair.
For the commands around these, such as moving keys between servers or locking down their directories, our SSH cheat sheet and Linux commands cheat sheet for Ubuntu and Rocky Linux cover the rest. If you need a newer OpenSSL than your distro ships, we have guides to building OpenSSL on Ubuntu and on Rocky Linux.