Appendix

Cheat sheet: the certificate commands you actually need

In one sentence

Tested OpenSSL, curl, keytool, Windows and Kubernetes commands for everyday certificate work, grouped by the task you are trying to do.

Who is this for

Level: Practitioner

On this page
  1. How do I inspect a certificate file?
  2. How do I see the chain a server sends?
  3. When does it expire?
  4. How do I create a key and a CSR?
  5. How do I convert between formats?
  6. How do I test a chain?
  7. Does this key match this certificate?
  8. How do I look inside a Java keystore?
  9. What should I never do?

Every command on this page was run for real, and every output block is real output, sometimes shortened where marked. The tools used were OpenSSL 3.2 and curl 8.10 in Git Bash on Windows, keytool from a Java 21 runtime, and Windows’ own certutil and PowerShell 5.1. Newer versions print the same information, sometimes with small differences in layout.

How do I inspect a certificate file?

Print everything, or only the fields you need:

openssl x509 -in cert.pem -noout -text
openssl x509 -in cert.pem -noout -subject -issuer -dates -ext subjectAltName
subject=CN=www.example.com
issuer=CN=www.example.com
notBefore=Oct 19 22:46:10 2026 GMT
notAfter=Jan 17 22:46:10 2027 GMT
X509v3 Subject Alternative Name: 
    DNS:www.example.com, DNS:example.com

Get the SHA-256 fingerprint, which identifies this exact certificate:

openssl x509 -in cert.pem -noout -fingerprint -sha256

If the file is binary (DER), add -inform DER. If OpenSSL says Could not find certificate, the file is probably DER, PKCS #12 or PKCS #7; see the file formats appendix.

On Windows, certutil reads PEM and DER files alike (output shortened):

certutil -dump le.cer
Issuer:
    CN=YE2
 NotBefore: 2026-09-04 4:34 PM
 NotAfter: 2026-12-03 4:34 PM
Subject:
    CN=letsencrypt.org
        DNS Name=cp.letsencrypt.org
        DNS Name=cp.root-x1.letsencrypt.org

certutil prints times in your local time zone. PowerShell can load the certificate as a .NET object:

$c = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("C:\certs\le.cer")
$c | Format-List Subject, Issuer, NotBefore, NotAfter, Thumbprint
Subject    : CN=letsencrypt.org
Issuer     : CN=YE2, O=Let's Encrypt, C=US
NotBefore  : 2026-09-04 4:34:32 PM
NotAfter   : 2026-12-03 3:34:31 PM
Thumbprint : 4AD7818D0F13687181FDED3B88D1F06561D580C7

The Windows “thumbprint” is the SHA-1 fingerprint. It identifies a certificate in the Windows stores; it says nothing about how secure the certificate is.

Or paste the certificate into the decoder:

Try it: Certificate decoderOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

PEM text, several PEM blocks, or a .pem, .crt, .cer or .der file (drop it on the box). Certificates are public, so they are safe to paste.

How do I see the chain a server sends?

openssl s_client -showcerts prints every certificate the server sends, in order (PEM blocks removed here):

openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -showcerts </dev/null
Certificate chain
 0 s:CN=letsencrypt.org
   i:C=US, O=Let's Encrypt, CN=YE2
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
   v:NotBefore: Sep  4 14:34:32 2026 GMT; NotAfter: Dec  3 14:34:31 2026 GMT
 1 s:C=US, O=Let's Encrypt, CN=YE2
   i:C=US, O=ISRG, CN=Root YE
   a:PKEY: id-ecPublicKey, 384 (bit); sigalg: ecdsa-with-SHA384
   v:NotBefore: Sep  3 00:00:00 2025 GMT; NotAfter: Sep  2 23:59:59 2028 GMT
 2 s:C=US, O=ISRG, CN=Root YE
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X2
   ...
 3 s:C=US, O=Internet Security Research Group, CN=ISRG Root X2
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
   ...

Read it as pairs: each certificate’s issuer (i:) should be the next certificate’s subject (s:). Always pass -servername: it sends SNI, and without it many servers return a default certificate for a different name. Certificates 2 and 3 here are cross-signed roots, sent so that clients with older trust stores can still build a path.

For a one-line verdict, use -brief:

openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
Peer certificate: CN=letsencrypt.org
Hash used: SHA256
Signature type: ECDSA
Verification: OK
Server Temp Key: X25519, 253 bits

With curl, --write-out '%{certs}' prints the chain the server sent (curl 7.88 and later; shortened):

curl -s https://letsencrypt.org/ -o /dev/null -w "%{certs}" | grep -E "^(Subject|Issuer|Expire Date):"
Subject:CN=letsencrypt.org
Issuer:C=US, O=Let's Encrypt, CN=YE2
Expire Date:2026-12-03 14:34:31 GMT
Subject:C=US, O=Let's Encrypt, CN=YE2
Issuer:C=US, O=ISRG, CN=Root YE
...

With Java’s keytool (shortened):

keytool -printcert -sslserver letsencrypt.org:443
Certificate #0
Owner: CN=letsencrypt.org
Issuer: CN=YE2, O=Let's Encrypt, C=US
Valid from: Fri Sep 04 16:34:32 CEST 2026 until: Thu Dec 03 15:34:31 CET 2026
Certificate #1
Owner: CN=YE2, O=Let's Encrypt, C=US
Issuer: CN=Root YE, O=ISRG, C=US

Paste a chain into the chain checker to see which link is missing or out of order:

Try it: Chain checkerOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

Every certificate the server sends, leaf first. The output of openssl s_client -showcerts -connect example.com:443 works as it is: we ignore the text around the certificates.

When does it expire?

For a file, print the end date, or let OpenSSL answer “will it still be valid in N seconds?” (2,592,000 seconds is 30 days). The exit code makes it easy to script:

openssl x509 -in cert.pem -noout -enddate
openssl x509 -in cert.pem -noout -checkend 2592000; echo "exit=$?"
notAfter=Jan 17 22:46:10 2027 GMT
Certificate will not expire
exit=0

When the certificate expires within that window, it prints Certificate will expire and exits with 1.

For a live server, pipe s_client into x509:

openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -enddate

On Windows, list the certificates in your personal store that expire within 30 days:

Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(30) } |
  Format-Table Subject, NotAfter, Thumbprint

Use Cert:\LocalMachine\My for the machine store that services such as IIS use. Plan renewals with the lifetime calendar at /tools/lifetime/, and see chapter 19 for inventory and monitoring.

In Kubernetes

A TLS secret stores the certificate base64-encoded under tls.crt. Decode it and hand it to OpenSSL. The command below builds the secret locally with a client-side dry run, so it needs no cluster; against a real cluster, replace the first part with kubectl get secret www-example-tls -n <namespace> -o jsonpath='{.data.tls\.crt}':

kubectl create secret tls www-example-tls --cert=leaf.pem --key=key.pem \
  --dry-run=client -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -enddate
subject=CN=www.example.com
notAfter=Jan 17 22:46:46 2027 GMT

With cert-manager installed, these show the state of every managed certificate and force a renewal. They need a cluster, so they were not run for this page; the syntax is from the cert-manager documentation:

kubectl get certificate -A
kubectl get certificaterequest -n my-namespace
cmctl status certificate my-certificate -n my-namespace
cmctl renew my-certificate -n my-namespace

How do I create a key and a CSR?

Generate the key where it will be used, then a CSR that carries the names in the subject alternative name. Browsers and Go ignore the common name for hostname checks. Some libraries, such as OpenSSL-based clients, Python and Node.js, fall back to it only when the certificate has no DNS name in its SAN (no SAN at all, or only IP addresses). So put every name in the SAN:

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out key.pem
openssl req -new -key key.pem -out req.csr -subj "/CN=www.example.com" \
  -addext "subjectAltName=DNS:www.example.com,DNS:example.com"

For RSA instead of ECDSA, use openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out key.pem. Check the CSR before you send it:

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

With keytool, the key pair is created inside a keystore, and the CSR comes from there:

keytool -genkeypair -alias app -keyalg EC -groupname secp256r1 -dname "CN=app.example.com" \
  -ext "SAN=dns:app.example.com" -validity 30 -keystore app.p12 -storetype PKCS12
keytool -certreq -alias app -keystore app.p12 -ext "SAN=dns:app.example.com" -file app.csr

Repeat the -ext option in -certreq: the SAN is not copied into the CSR from the temporary self-signed certificate. When the CA returns the certificate, import it under the same alias with keytool -importcert -alias app -file app.crt -keystore app.p12, after importing the CA’s intermediate certificates. Read chapter 10 for what belongs in a CSR, and decode one here:

Try it: CSR decoderOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere. A CSR is public; never paste the private key that goes with it.

PEM text, or a .csr file dropped on the box.

How do I convert between formats?

The file formats appendix explains each format. The everyday conversions:

# PEM to DER, and back
openssl x509 -in cert.pem -outform DER -out cert.der
openssl x509 -in cert.der -inform DER -out cert.pem

# certificate + key (+ chain) into one PKCS #12 / PFX file
openssl pkcs12 -export -in cert.pem -inkey key.pem -certfile chain.pem -name www.example.com -out bundle.p12

# PKCS #12 back to PEM: certificates only, then the key into its own file
openssl pkcs12 -in bundle.p12 -nokeys -out certs.pem
openssl pkcs12 -in bundle.p12 -nocerts -noenc -out key.pem

# PKCS #7 / P7B to PEM certificates
openssl pkcs7 -in bundle.p7b -print_certs -out certs.pem

-noenc (called -nodes before OpenSSL 3.0) writes the key without a password. Do that only when the software that reads it cannot handle an encrypted key, and protect the file.

OpenSSL 3 encrypts PKCS #12 files with AES-256 by default. Some older systems cannot open those. openssl pkcs12 -export -legacy writes the old format instead, which uses weak 40-bit RC2 encryption for the certificates, so use it only when you must, and never send such a file over an untrusted channel.

On Windows, certutil converts between binary DER and Base64 (PEM-style) files:

certutil -encode le.cer le-b64.cer
certutil -decode le-b64.cer le-der.cer
CertUtil: -encode command completed successfully.
CertUtil: -decode command completed successfully.

For Java keystores, see the keytool section below.

How do I test a chain?

openssl verify takes the trusted root with -CAfile and any intermediates with -untrusted:

openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
openssl verify -CAfile root.pem leaf.pem
leaf.pem: OK
CN=www.example.com
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf.pem: verification failed

The second command leaves out the intermediate, which is exactly what a client sees when a server forgets to send it. That is error 20, and curl reports the same problem as unable to get local issuer certificate. Add a hostname check to catch name mismatches:

openssl verify -CAfile root.pem -untrusted intermediate.pem -verify_hostname shop.example.com leaf.pem
CN=www.example.com
error 62 at 0 depth lookup: hostname mismatch
error leaf.pem: verification failed

On Windows, certutil -verify -urlfetch builds the chain with the Windows trust store, downloads missing intermediates and checks revocation (output shortened):

certutil -verify -urlfetch le.cer
Verified Application Policies:
    1.3.6.1.5.5.7.3.1 Server Authentication
Cert is an End Entity certificate
Leaf certificate revocation check passed
CertUtil: -verify command completed successfully.

For a certificate from a private CA that Windows does not trust, the same command reports CERT_E_CHAINING (“A certificate chain could not be built to a trusted root authority”).

Does this key match this certificate?

A certificate, its CSR and its private key match when they contain the same public key. Hash the public key from each and compare:

openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in key.pem -pubout | openssl sha256
openssl req -in req.csr -noout -pubkey | openssl sha256
SHA2-256(stdin)= 0459fcddca0c5251a423f21a81a1292bfedbdbd413bc4cbf58ad8981961436a8
SHA2-256(stdin)= 0459fcddca0c5251a423f21a81a1292bfedbdbd413bc4cbf58ad8981961436a8
SHA2-256(stdin)= 0459fcddca0c5251a423f21a81a1292bfedbdbd413bc4cbf58ad8981961436a8

Identical hashes mean a match. This works for RSA and EC keys alike and never prints the private key itself. A web server that refuses to start with “key values mismatch” or “private key does not match” fails this test.

How do I look inside a Java keystore?

List the entries, or show every certificate in detail (shortened):

keytool -list -keystore bundle.p12 -storepass changeit
keytool -list -v -keystore bundle.p12 -storepass changeit
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 1 entry

www.example.com, Oct 20, 2026, PrivateKeyEntry, 
Certificate fingerprint (SHA-256): 78:AA:D2:0B:61:70:AA:36:10:CF:FC:4D:14:8A:93:91:FF:28:9E:27:0B:E7:A2:5E:5A:0F:28:DC:57:CD:6D:70

A PrivateKeyEntry holds a key and its certificate chain (a server’s or client’s identity). A trustedCertEntry holds a CA certificate that Java should trust. The JDK’s own trust store is reached with -cacerts; its default password is changeit:

keytool -list -cacerts -storepass changeit
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 109 entries

The number of entries depends on your Java version. To trust a private CA in your own trust store (rather than editing the JDK’s), import its root:

keytool -importcert -noprompt -alias example-root -file root.pem -keystore truststore.p12 -storetype PKCS12 -storepass changeit
Certificate was added to keystore

Then point the application at it, for example with -Djavax.net.ssl.trustStore=truststore.p12. This is the real fix for PKIX path building failed.

Convert an old JKS keystore to PKCS #12, the standard format that current JDKs use by default:

keytool -importkeystore -srckeystore legacy.jks -srcstorepass changeit \
  -destkeystore converted.p12 -deststoretype PKCS12 -deststorepass changeit

keytool itself suggests this when you create a JKS file:

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore legacy2.jks -destkeystore legacy2.jks -deststoretype pkcs12".

What should I never do?

  • Do not email or upload private keys. Move a key only inside an encrypted PKCS #12 file with a strong password, and only when you must.
  • Do not reuse one key everywhere. Generate a new key for each server or service and for each renewal, unless you have a reason not to.
  • Do not leave unencrypted keys world-readable. On Linux, chmod 600 key.pem.

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9