package main import ( "crypto/ecdsa" "crypto/x509" "encoding/pem" "fmt" "os" "time") func main() { pemBytes, _ := os.ReadFile("cert.pem") block, _ := pem.Decode(pemBytes) // type "CERTIFICATE" cert, err := x509.ParseCertificate(block.Bytes) if err != nil { panic(err) } fmt.Println("subject:", cert.Subject) fmt.Println("issuer:", cert.Issuer) fmt.Println("valid:", cert.NotBefore, "->", cert.NotAfter) fmt.Println("serial:", cert.SerialNumber) fmt.Println("sig alg:", cert.SignatureAlgorithm) // ECDSA-SHA256 if pub, ok := cert.PublicKey.(*ecdsa.PublicKey); ok { fmt.Println("EC key on", pub.Curve.Params().Name) // P-256 } fmt.Println("days until expiry:", int(time.Until(cert.NotAfter).Hours()/24))}How it works
pem.Decodereturns the first block and the rest of the input — loop on the rest to handle bundle files with a whole chain.cert.PublicKeyis typedany; a type switch over*ecdsa.PublicKey,*rsa.PublicKeyanded25519.PublicKeycovers real-world certs.NotBefore/NotAfteraretime.Time— expiry math is ordinary time arithmetic, no string parsing.- Subject alternative names are pre-parsed:
cert.DNSNames,cert.IPAddressesandcert.EmailAddresses— no need to dig throughcert.Extensionsfor the common cases.
Gotchas
pem.Decodereturnsnil(not an error) when the input has no PEM block — a DER file, a typo'd path. Checkblock != nilbefore touchingblock.Bytesor you trade a clear message for a nil-pointer panic.cert.SignatureAlgorithmdescribes how the CA signed this certificate; the certificate's own key type lives incert.PublicKeyAlgorithmandcert.PublicKey. ECDSA leaf, RSA chain is common.- For trust decisions use
cert.Verify(x509.VerifyOptions{...})with a root pool —ParseCertificateaccepts expired and self-signed certificates by design.