How to Parse and Generate X.509 Certificates in Go
You are spinning up a local development server and the browser refuses to connect because the certificate is self-signed and expired. Or you are writing a tool that audits certificates across a fleet of servers and needs to read the validity dates without shelling out to OpenSSL. You cannot rely on a static file on disk. You need to generate, sign, and parse certificates directly in your Go program. Go ships with crypto/x509 to handle the heavy lifting of the X.509 standard.
Concept and structure
An X.509 certificate is a structured document that binds a public key to an identity. It contains metadata about the owner, a validity window, and a digital signature from a trusted authority. Go represents this document as an x509.Certificate struct. You populate the struct with the details, sign it with a private key, and encode it into bytes.
Think of a certificate like a passport. The struct is the passport application form where you fill in your name and photo. The private key is the government's official stamp. The resulting bytes are the passport itself. Anyone can verify the stamp using the government's public seal, which is the parent certificate's public key.
The X.509 standard uses ASN.1 encoding, a complex binary format for describing data structures. Go hides the ASN.1 details behind the struct. You work with fields like Subject and NotAfter, and the library handles the marshaling to binary and back. This abstraction prevents subtle encoding bugs while giving you full control over the certificate contents.
Minimal example
Here is the simplest way to generate a self-signed certificate. You generate a key, define a template, sign the template, and encode the result to PEM.
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"time"
)
func main() {
// Generate a 2048-bit RSA key pair for signing
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
// Template defines metadata; SerialNumber must be positive and unique
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "localhost"},
NotBefore: time.Now().UTC(),
NotAfter: time.Now().UTC().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
}
The template sets the constraints. SerialNumber must be unique. Subject identifies the owner. NotBefore and NotAfter define the validity window. KeyUsage restricts the key operations. BasicConstraintsValid marks the certificate as a valid CA or end-entity cert.
import (
"encoding/pem"
"fmt"
)
// CreateCertificate signs the template; passing &template as parent makes it self-signed
derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
// Encode DER bytes to PEM format for human-readable output
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
fmt.Println(string(pemBytes))
x509.CreateCertificate takes the template and the parent certificate. Passing the same template for both arguments creates a self-signed certificate. The function returns DER bytes. pem.EncodeToMemory wraps those bytes in base64 text with headers.
Certificates are binary blobs until you parse them. Treat the struct as the source of truth.
Walk through what happens
The x509.Certificate struct holds every field defined by the X.509 standard. SerialNumber must be unique for every certificate issued by the same authority. Subject identifies the owner using a distinguished name. NotBefore and NotAfter set the validity window. KeyUsage restricts what the key can do. BasicConstraintsValid marks the certificate as a valid certificate.
x509.CreateCertificate takes five arguments. The first is a random source. The second is the template. The third is the parent certificate. The fourth is the public key. The fifth is the private key. The function marshals the template to DER, hashes it, signs the hash with the private key, and attaches the signature.
The result is DER bytes. DER is the raw binary format. PEM is a base64 encoding of DER with headers. pem.EncodeToMemory converts DER to PEM. Most tools expect PEM. Browsers expect PEM. Humans expect PEM.
The community accepts verbose error handling because it makes the unhappy path visible. if err != nil { return err } is the standard pattern. Do not hide errors. Do not use _ to discard errors from certificate operations. The underscore discards a value intentionally, signaling you considered the return value and chose to drop it. Use it sparingly with errors.
Trust the parser, verify the fields.
Realistic example
Parsing a certificate is the reverse of generating one. You take DER bytes and get a *x509.Certificate struct. You can read any field from the struct.
// ParseCertificate takes DER bytes and returns a Certificate struct
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
panic(err)
}
// Access parsed fields directly from the struct
fmt.Println("Subject:", cert.Subject.CommonName)
fmt.Println("Valid from:", cert.NotBefore)
fmt.Println("Valid until:", cert.NotAfter)
Parsing does not verify the certificate. It only checks the structure. To verify the signature, you call cert.CheckSignatureFrom(parentCert). This checks if the parent certificate's public key signed this certificate.
// CheckSignatureFrom verifies the certificate was signed by the parent
err = cert.CheckSignatureFrom(parentCert)
if err != nil {
// Signature mismatch or algorithm error
panic(err)
}
Modern certificates rely on Subject Alternative Names. The struct exposes DNSNames and IPAddresses slices. You can iterate over these to check if the certificate covers a specific hostname.
// DNSNames contains the Subject Alternative Names for DNS
fmt.Println("SANs:", cert.DNSNames)
Parsing is cheap. Verification is the work. Always verify before you trust.
Pitfalls and errors
Time zones break certificates. time.Now() uses local time. Certificates require UTC. Use time.Now().UTC(). If you use local time, the certificate might be invalid in other time zones. The compiler does not catch this. The runtime will reject the certificate with x509: certificate has expired or is not yet valid.
Subject Alternative Names are mandatory for modern browsers. Browsers ignore the Common Name. You must set DNSNames or IPAddresses in the template. If you forget, the browser rejects the certificate with x509: certificate relies on legacy Common Name field.
Serial numbers must be unique. Hardcoding big.NewInt(1) causes collisions. Generate a random serial number. Use rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) to generate a 128-bit random serial number.
Key usage flags restrict the key. Forgetting KeyUsageDigitalSignature breaks TLS. Forgetting KeyUsageKeyEncipherment breaks RSA key exchange. Set the flags explicitly. The compiler rejects the program with x509: invalid key usage if you try to use the key for the wrong purpose.
Key size matters. RSA 2048 is the current standard. RSA 1024 is broken and rejected by modern stacks. ECDSA P-256 offers smaller keys and faster operations. Choose the algorithm based on your performance and compatibility requirements.
The worst certificate bug is the one that expires silently. Set the serial number randomly and check the SANs.
Decision matrix
Use crypto/x509 when you need to create or inspect certificates programmatically. Use crypto/tls when you need to run a server or client with TLS. Use x509.ParseCertificate when you have raw DER bytes. Use pem.Decode when you have a PEM-encoded string or file. Use x509.CreateCertificate when you need to sign a certificate template. Use rsa.GenerateKey when you need an RSA key pair. Use ecdsa.GenerateKey when you need an ECDSA key pair for smaller keys and faster operations.
Pick the tool that matches the data format. DER for bytes, PEM for text, x509 for logic.