Skip to content
ecdsa.com

Certificate expiry · Go

How to check SSL certificate expiration date in Go

To check SSL certificate expiration date in Go, parse the file with crypto/x509 and read NotAfter, or dial the server with crypto/tls and read NotAfter from ConnectionState().PeerCertificates. This recipe does both with the standard library, reports the earliest expiry along the verified chain, and shows the error a verifying client gets from an expired certificate.

Tested with Go 1.27.3 (crypto/x509 and crypto/tls, linux/arm64) and OpenSSL 3.5.7 (openssl s_server as the local TLS servers).

expiry.go — notAfter from a PEM file and from a TLS server
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"net"
"os"
"time"
)
// fileNotAfter reads the first certificate of a PEM file.
func fileNotAfter(path string) (time.Time, error) {
data, err := os.ReadFile(path)
if err != nil {
return time.Time{}, err
}
block, _ := pem.Decode(data)
if block == nil || block.Type != "CERTIFICATE" {
return time.Time{}, errors.New(path + ": no CERTIFICATE block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return time.Time{}, err
}
return cert.NotAfter, nil // UTC
}
// serverNotAfter connects with full verification and returns the leaf's expiry
// and the earliest expiry along the verified chain.
func serverNotAfter(addr, serverName string, roots *x509.CertPool) (leaf, chain time.Time, err error) {
dialer := &net.Dialer{Timeout: 10 * time.Second}
conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{RootCAs: roots, ServerName: serverName})
if err != nil {
return leaf, chain, err
}
defer conn.Close()
state := conn.ConnectionState()
leaf, chain = state.PeerCertificates[0].NotAfter, state.PeerCertificates[0].NotAfter
for _, cert := range state.VerifiedChains[0] {
if cert.NotAfter.Before(chain) {
chain = cert.NotAfter
}
}
return leaf, chain, nil
}
main.go — both files, a valid server and an expired one
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"os"
"time"
)
func main() {
for _, path := range []string{"leaf.pem", "expired.pem"} {
notAfter, err := fileNotAfter(path)
if err != nil {
log.Fatal(err)
}
days := int(time.Until(notAfter).Hours() / 24)
fmt.Printf("%s: notAfter %s, %d days left\n", path, notAfter.Format(time.RFC3339), days)
}
roots := x509.NewCertPool() // only our test root; nil would mean the system roots
rootPEM, err := os.ReadFile("root.pem")
if err != nil || !roots.AppendCertsFromPEM(rootPEM) {
log.Fatal("cannot load root.pem")
}
leaf, chain, err := serverNotAfter("api.example.com:8443", "api.example.com", roots)
if err != nil {
log.Fatal(err)
}
fmt.Println("api.example.com: leaf", leaf.Format(time.RFC3339), "chain", chain.Format(time.RFC3339))
// An expired certificate fails verification, so read it without verifying — only to look at it
_, _, err = serverNotAfter("expired.example.com:9443", "expired.example.com", roots)
fmt.Println("verified:", err)
conn, err := tls.Dial("tcp", "expired.example.com:9443", &tls.Config{InsecureSkipVerify: true})
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Println("unverified: notAfter", conn.ConnectionState().PeerCertificates[0].NotAfter.Format(time.RFC3339))
}

How it works

  1. pem.Decode returns the first block; x509.ParseCertificate(block.Bytes) gives a *x509.Certificate whose NotAfter is a time.Time in UTC.
  2. tls.DialWithDialer with RootCAs and ServerName verifies the chain and the host name (and sends SNI); a nil RootCAs means the system roots.
  3. VerifiedChains[0] is the chain Go built to a trusted root. The chain stops working as soon as any of its certificates expires, so serverNotAfter also returns the earliest NotAfter along it.
  4. time.Until(notAfter) gives the remaining duration; the run prints 2035-12-31T23:59:59Z for the leaf from the file and from the local test server.

Gotchas

  • A verifying tls.Dial to an expired certificate fails with tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time … is after 2025-03-31T23:59:59Z — the date is in the error, but you get no ConnectionState.
  • To inspect such a certificate, dial with InsecureSkipVerify: true and read PeerCertificates[0]: the date is there (2025-03-31T23:59:59Z), the chain is not verified, and the connection must carry nothing else.
  • int(time.Until(t).Hours() / 24) truncates toward zero: the expired certificate shows -567 days where flooring gives -568. Compare time.Time values (notAfter.Before(deadline)) instead of day counts when the boundary matters.
  • pem.Decode silently returns nil for a file without PEM blocks — check it, as fileNotAfter does, before calling ParseCertificate.

Related recipes