Bridge SSL

OpenSSL commands to check and verify a certificate

13 min readUpdated

OpenSSL can read any certificate, show its dates and names, convert it between formats, check that it belongs to its private key, decode a signing request and verify that it chains to a trusted root. This reference, part of the SSL certificates section, groups those commands by task, with the output each prints and how to read it.

To test one live hostname step by step, start from the SSL tools section. Without a terminal, a browser opens the certificate from the padlock: see how to view Chrome certificates and the Firefox and Safari equivalents.

Quick facts

Read a file
openssl x509 -in cert.pem -noout -text
Expiry date
openssl x509 -in cert.pem -noout -enddate
DER to PEM
openssl x509 -in cert.der -inform DER -out cert.pem
Certificate and key match
the same digest from openssl x509 -in cert.pem -noout -pubkey | openssl sha256 and openssl pkey -in key.pem -pubout | openssl sha256
Check a CSR
openssl req -in example.com.csr -noout -text -verify
Verify a chain
openssl verify -CAfile root.pem -untrusted intermediate.pem cert.pem
Live server
openssl s_client -connect example.com:443 -servername example.com

Examples use placeholder names. The output comes from OpenSSL 3.0.13 and test certificates issued by a private test root, Example Root CA; on a real site the names, dates and issuer are that site's.

View a certificate file

Before you install a certificate or verify its chain, decode it:

$ openssl x509 -in cert.pem -noout -text

-text prints every field: subject, issuer, validity dates, public key, extensions and signature algorithm. -noout suppresses the encoded certificate that would otherwise follow [1]. To print only some fields, name them:

$ openssl x509 -in cert.pem -noout -subject -issuer -ext subjectAltName
subject=CN = example.com
issuer=CN = Example Intermediate CA
X509v3 Subject Alternative Name: 
    DNS:example.com, DNS:www.example.com

The names that count are under Subject Alternative Name, not the subject: Chrome stopped matching the common name in version 58 [7]. See Subject Alternative Names.

Check the expiry date

$ openssl x509 -in cert.pem -noout -dates
notBefore=Oct 15 11:18:26 2026 GMT
notAfter=Jan 13 11:18:26 2027 GMT

notAfter is the moment the certificate stops being accepted. For a script or a monitoring check, -checkend answers yes or no instead: will the certificate expire within this many seconds?

$ openssl x509 -in cert.pem -noout -checkend 2592000
Certificate will not expire

2592000 seconds is 30 days. The command exits with status 0 when the certificate stays valid for that long and 1 when it does not, printing Certificate will expire [1], so a cron job can act on the exit status.

Convert between PEM, DER and PFX

To tell the encoding, look at the first line. A file that starts with -----BEGIN CERTIFICATE----- is PEM, the text encoding described in RFC 7468; the same kind of line marks a signing request (BEGIN CERTIFICATE REQUEST) and a private key (BEGIN PRIVATE KEY) [4]. A binary file, often with a .der or .cer extension, is DER. OpenSSL 3 tries every input format on its own [1]. In 1.1.1 the default input format is PEM, so DER needs -inform DER, which both versions accept [1][3].

Conversion commands

Conversion commands
TaskCommand
PEM to DERopenssl x509 -in cert.pem -outform DER -out cert.der
DER to PEMopenssl x509 -in cert.der -inform DER -out cert.pem
PFX to PEM, certificates onlyopenssl pkcs12 -in bundle.pfx -nokeys -out certs.pem
PFX to PEM, private key onlyopenssl pkcs12 -in bundle.pfx -nocerts -noenc -out key.pem
PEM to PFXopenssl pkcs12 -export -in cert.pem -inkey key.pem -certfile intermediate.pem -out bundle.pfx
P7B to PEMopenssl pkcs7 -print_certs -in chain.p7b -out chain.pem

The pkcs12 commands ask for the bundle's password. -noenc writes the key unencrypted. It replaces -nodes, which the OpenSSL 3.0 manual marks deprecated [1] and 3.0.13 still accepts with the same result; the 1.1.1 manual lists only -nodes [3]. Converting the test certificate to DER and back gave a byte-identical file, checked with cmp. Which format a server expects, and the conversions that break an installation, are on certificate formats.

Check that a certificate and key match

A certificate works only with the private key it was issued for: with another key, the test server refused to start with key values mismatch. Derive the public key from each file and compare the digests; identical lines mean a pair [1]:

$ openssl x509 -in cert.pem -noout -pubkey | openssl sha256
SHA2-256(stdin)= 7bea11aa82fc41aee3f982934898609e4c794b19277fc617c70ddd6294645e9d
$ openssl pkey -in key.pem -pubout | openssl sha256
SHA2-256(stdin)= 7bea11aa82fc41aee3f982934898609e4c794b19277fc617c70ddd6294645e9d

A signing request is compared the same way: openssl req -in example.com.csr -noout -pubkey | openssl sha256. Why the pair has to match, and what a mismatch looks like on a server, is on the certificate and key matcher page.

Create and check a CSR

One command creates a key and a signing request that lists every name:

$ openssl req -new -newkey rsa:2048 -noenc -keyout example.com.key -out example.com.csr -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:www.example.com"

-addext adds the extension to the request [1]. Before submitting, read the request back. -verify checks the request's own signature:

$ openssl req -in example.com.csr -noout -subject -verify
Certificate request self-signature verify OK
subject=CN = example.com
$ openssl req -in example.com.csr -noout -text | grep -A1 "Subject Alternative Name"
                X509v3 Subject Alternative Name: 
                    DNS:example.com, DNS:www.example.com

A request without the Subject Alternative Name line declares only the common name. The key file is the private key and stays on the server. What to put in each field is on certificate signing request.

Verify a chain from files

Before installing, check that a certificate chains to a root:

$ openssl verify -CAfile root.pem -untrusted intermediate.pem cert.pem
cert.pem: OK

-CAfile holds the root you trust. -untrusted holds the intermediates, which are used to build the path but are not trusted on their own [1]. Leave the intermediate out and the check fails at depth 0:

$ openssl verify -CAfile root.pem cert.pem
CN = example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error cert.pem: verification failed

Depth 0 is the certificate being checked, 1 the one that signed it, and so on [1]. To check the name too, add -verify_hostname example.com; a name it does not cover fails with error 62. How a complete chain is assembled is on certificate chains.

Which trust store OpenSSL uses

Without -CAfile, OpenSSL loads its default trust store: the file cert.pem and the directory certs in its OpenSSL directory [6]. OpenSSL itself ships no trust anchors; many Linux distributions install a set and point OpenSSL to it [1]. Two machines can therefore disagree about one chain. openssl version prints the version, and -d the directory:

$ openssl version -d
OPENSSLDIR: "/usr/lib/ssl"

Connect to a live server

A file on disk shows what you installed; visitors receive what the server sends:

$ openssl s_client -connect example.com:443 -servername example.com </dev/null

-connect names the host and port. -servername sets the hostname sent in the handshake (SNI), which a server with several sites uses to choose a certificate. Since OpenSSL 1.1.1 the name from -connect is sent by default when it is a DNS name [1], so -servername matters when you connect by IP address or test one hostname through another. </dev/null closes the connection once the handshake completes instead of waiting for input.

To read the served certificate's dates and names, pipe the output into x509:

$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -ext subjectAltName

For a mail server that switches to TLS after connecting, add -starttls smtp, for example on the submission port 587 defined in RFC 6409 [5]; -starttls also takes pop3, imap, ftp and other protocols [1].

Show the chain a server sends

$ openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null

-showcerts prints every certificate the server sends, in order; this is not a verified chain [1]. The summary lists them by position:

Certificate chain
 0 s:CN = example.com
   i:CN = Example Intermediate CA
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Oct 15 11:18:26 2026 GMT; NotAfter: Jan 13 11:18:26 2027 GMT
 1 s:CN = Example Intermediate CA
   i:CN = Example Root CA
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Oct 15 11:18:26 2026 GMT; NotAfter: Oct 14 11:18:26 2031 GMT

Position 0 is the site's own certificate; s: is its subject, i: its issuer, a: the key and signature algorithm, v: the dates. Each issuer should be the subject of the next line. A list that stops at position 0 means the server sends no intermediate, and the output ends with Verify return code: 21 (unable to verify the first certificate).

Test one protocol version

$ openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
$ openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null

Each command requires that one version [1]. A completed handshake names it on the New, TLSv1.2, Cipher is … line. A refused one prints New, (NONE), Cipher is (NONE), and an alert such as tlsv1 alert protocol version goes to standard error, so it shows near the top of the output. The Protocol line of the session block repeats the requested version either way, so it proves nothing.

Read the verify return codes

openssl verify reports a failure as error <code> at <depth> depth lookup: <text>; openssl s_client ends with Verify return code: <code> (<text>). The numbers and texts come from OpenSSL's list of verification errors [2]; each row was reproduced on the test setup.

Read the verify return codes
CodeTextWhat it means
0okthe certificate chains to a trusted root
9certificate is not yet validits notBefore date is after the current time
10certificate has expireda certificate in the chain is past its notAfter date; see certificate renewal
18self-signed certificatethe certificate signed itself and is not in the trust store; see self-signed certificates
19self-signed certificate in certificate chainthe chain was built, but its root is not in the trust store
20unable to get local issuer certificatean issuer was not found: at depth 0 the intermediate is missing, at a higher depth the root is not trusted
21unable to verify the first certificateonly one certificate was available, not self-signed, with no issuer to check it
62hostname mismatchthe certificate does not cover the name checked with -verify_hostname

When s_client keeps going

s_client completes the connection even when the code is not 0 [1], so read the code: a printed certificate does not mean a valid one. -verify_return_error makes it stop at the failure instead; against a server that sent no intermediate, it then exited with status 1 rather than 0.

FAQ

How do I check a certificate's expiry date with OpenSSL?

For a file, run openssl x509 -in cert.pem -noout -enddate. For a live site, pipe openssl s_client -connect example.com:443 -servername example.com into openssl x509 -noout -dates.

How do I verify a certificate chain with OpenSSL?

Run openssl verify -CAfile root.pem -untrusted intermediate.pem cert.pem. It prints OK, or an error code, depth and text when the chain does not reach the root.

How do I see the certificate a website is serving?

Run openssl s_client -connect example.com:443 -servername example.com -showcerts. It prints each certificate the server sends, then the verification result.

What does "Verify return code: 21 (unable to verify the first certificate)" mean?

The server sent its certificate without the intermediate that signed it, so no path to a trusted root could be built. Install the full chain.

Why do I need -servername with s_client?

It sets the hostname sent in the handshake, which a server with several sites uses to pick the certificate. Since 1.1.1 OpenSSL sends the -connect name itself, so -servername matters for IP addresses and older versions.

How do I check a certificate in DER format?

Run openssl x509 -in cert.der -inform DER -noout -text. OpenSSL 3 detects DER without the option; 1.1.1 assumes PEM.

Sources

  1. OpenSSL 3.0 manual pages: openssl-x509, openssl-s_client, openssl-verify, openssl-req, openssl-pkey, openssl-pkcs12, openssl-pkcs7, openssl-version, openssl-format-options, openssl-verification-options, and openssl (TLS version options).

  2. OpenSSL 3.0, X509_STORE_CTX_get_error manual page — texts of the verification errors; include/openssl/x509_vfy.h.in — their numbers.

  3. OpenSSL 1.1.1, x509, req and pkcs12 manual pages — the default input format is PEM; -nodes leaves the private key unencrypted.

  4. IETF, RFC 7468, Textual Encodings of PKIX, PKCS, and CMS Structures.

  5. IETF, RFC 6409, Message Submission for Mail.

  6. OpenSSL 3.0, SSL_CTX_load_verify_locations manual page — the default CA file cert.pem and directory certs.

  7. Chrome for Developers, Deprecations and Removals in Chrome 58 — commonName matching in certificates removed.