skip to content

Certificates and x509 Chains

Turning PEM bytes into an x509.Certificate, putting roots in a CertPool and calling Verify yourself, plus what UnknownAuthorityError actually tells you. Asked whenever a chain refuses to build.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In Go, how do you turn the bytes of a PEM certificate file into an *x509.Certificate?

level: juniorimportance: must knowfreq 48%

answer

  1. two encodings, so two calls
  2. the outer layer is base64 text
  3. the inner payload is DER
  4. failure arrives as a nil block, not an error
  5. block.Bytes is what the parser wants

basics

~10 s

Call pem.Decode on the file bytes, check the returned block is non-nil and its Type is CERTIFICATE, then pass block.Bytes, which is the DER, to x509.ParseCertificate. Both steps can fail; check both.

solid answer

~40 s

It is two steps, because PEM and DER are two different encodings. `pem.Decode(data)` strips the `-----BEGIN CERTIFICATE-----` armour and base64-decodes the body, returning a `*pem.Block` plus the unconsumed `rest` of the input. The important detail is that it signals failure by returning a **nil block and no error** — so `if block == nil` is mandatory, and it is worth also checking `block.Type == "CERTIFICATE"` because a bundle may start with a private key or a CSR. Then `x509.ParseCertificate(block.Bytes)` parses the DER into an `*x509.Certificate` whose fields (`Subject`, `NotBefore`, `NotAfter`, `DNSNames`) you can read. If the file holds several concatenated blocks, feed `rest` back into `pem.Decode` in a loop until it returns nil.

code

go · 12 lines
go
block, rest := pem.Decode(pemBytes)
if block == nil {
	return nil, errors.New("input contains no PEM block")
}
if block.Type != "CERTIFICATE" {
	return nil, fmt.Errorf("first PEM block is %q, not CERTIFICATE", block.Type)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
	return nil, fmt.Errorf("parse certificate: %w", err)
}
_ = rest // remaining bytes; feed back into pem.Decode for the next block

go deeper

for a junior

Be ready to name both calls in order and to say what each one produces: pem.Decode gives a block, x509.ParseCertificate turns block.Bytes into a certificate. Remember that a nil block is how decoding reports failure.

for a middle

Explain why two steps exist at all — PEM is a base64 text envelope, DER is the binary ASN.1 the parser reads — and show the loop over the rest return that walks a fullchain file, skipping blocks whose Type is not CERTIFICATE.

for a senior

Show the error handling an operator can act on: report the path, the block type you actually found, and the underlying parse error, instead of panicking or returning a bare 'invalid certificate'. Loaders like this run at startup and their message is the whole incident.

for a principal

Own where certificate loading lives in the codebase: one reviewed helper that every service calls, rather than a hand-rolled decode in each package. Duplicated loaders are where the missing nil check and the ignored second block keep coming back.

## Two encodings, two steps A `.pem` or `.crt` file is almost never a certificate in the form Go's parser wants. A certificate is defined as a DER-encoded ASN.1 structure — pure binary. PEM is a text *envelope* around that binary: a `-----BEGIN CERTIFICATE-----` line, optional `Key: value` headers, the DER base64-encoded and wrapped at 64 columns, then `-----END CERTIFICATE-----`. So loading a certificate in Go is always two calls: one to strip the envelope, one to parse the payload. ``` file bytes --pem.Decode--> *pem.Block{Type, Headers, Bytes} block.Bytes --x509.ParseCertificate--> *x509.Certificate ``` ## Step one: pem.Decode The signature is `func Decode(data []byte) (p *pem.Block, rest []byte)`. Note what is *not* there: an `error`. This is the single most common trip-up. When the input contains no PEM block at all — someone pasted DER, or a base64 blob without the armour lines, or an HTML error page a proxy returned instead of the file — `Decode` returns `p == nil` and hands the whole input back as `rest`. Code that goes straight to `block.Bytes` panics with a nil pointer dereference, and the operator sees a stack trace instead of "that file is not PEM". The returned `pem.Block` has three fields: `Type` (the word between BEGIN and the dashes, e.g. `CERTIFICATE`, `RSA PRIVATE KEY`, `CERTIFICATE REQUEST`), `Headers` (a `map[string]string`, usually empty), and `Bytes` (the decoded DER). Always check `Type`: a real-world bundle from a customer frequently contains a key or a CSR alongside the certificates, and handing a private key's DER to the certificate parser produces a confusing ASN.1 error rather than "that is a key". ## Step two: x509.ParseCertificate `x509.ParseCertificate(der []byte) (*x509.Certificate, error)` takes exactly one certificate's DER. Passing it the whole PEM file — a mistake that compiles fine, because both are `[]byte` — fails with an ASN.1 structure error, since the base64 text is not valid DER. Its sibling `x509.ParseCertificates` (plural) parses *several* certificates concatenated as DER, which is the shape you get from a TLS handshake, not from a PEM file. On success you hold an `*x509.Certificate` whose fields are just data you can read: `Subject` and `Issuer` (both `pkix.Name`), `NotBefore` and `NotAfter` (`time.Time`), `SerialNumber` (`*big.Int`), `DNSNames` and `IPAddresses` for the subject alternative names, `IsCA`, and `Raw` (the original DER, useful if you want to re-encode it). ## Several blocks in one file A server certificate is usually shipped as a fullchain file: leaf, then intermediate, then perhaps a root, concatenated. That is what `rest` is for. Loop: decode, break when the block is nil, skip blocks whose `Type` is not `CERTIFICATE`, parse the rest, append. One practical gotcha when a chain was assembled by hand: if the `-----END-----` line of one block and the `-----BEGIN-----` line of the next end up on the same line, `pem.Decode` will not find the second block, and it fails silently by simply returning fewer certificates than you expected. ## Error handling that helps the person on the other end Because `pem.Decode` cannot tell you *why*, the useful thing is to say what you looked at: the path, the number of bytes read, and the first block type you found. A parse utility that prints `no CERTIFICATE block in bundle.pem (first block was RSA PRIVATE KEY)` ends the conversation immediately; one that panics on a nil pointer starts a different, longer one. ## Encoding back out The reverse direction is `pem.Encode(w io.Writer, b *pem.Block)` or `pem.EncodeToMemory(b)` with a block you build yourself: `&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}`. Round-tripping through `cert.Raw` rather than re-serialising is what keeps the signature valid, since re-encoding a parsed structure can produce different bytes.

  • What is the second value pem.Decode returns, and when do you need it?
    It is `rest`: everything after the block it just consumed. You need it whenever a file holds more than one block — a fullchain file with leaf plus intermediates, or a key and a certificate together. Loop `block, rest = pem.Decode(rest)` until `block` is nil. When no block is found at all, `rest` is the entire input unchanged.
  • What actually happens if you skip the nil check on the block pem.Decode returns?
    The program panics with a nil pointer dereference on `block.Bytes`. `pem.Decode` has no error return, so a file that is not PEM — DER by mistake, a truncated download, an HTML error page — produces a nil block that looks fine until the next line dereferences it. This is why the check is not optional stylistic care.
  • When would you reach for x509.ParseCertificates instead of x509.ParseCertificate?
    When you have several certificates concatenated as raw DER rather than as separate PEM blocks — for example bytes taken straight off the wire. `ParseCertificate` parses exactly one and errors on trailing data; `ParseCertificates` returns a `[]*x509.Certificate`. For a PEM file you still loop over `pem.Decode` first, because PEM blocks are text-delimited.

saying these in an interview costs you the question

  • Says pem.Decode returns an error when the input is not PEM
  • Passes the whole PEM file straight to x509.ParseCertificate
  • Dereferences block.Bytes without checking the block is non-nil
  • Assumes every PEM block in a file is a certificate
  • Treats PEM and DER as the same encoding
  • Ignores the rest return and silently loads only the leaf
open as a page

What does calling Verify on an *x509.Certificate with x509.VerifyOptions check, and what does it not?

level: middleimportance: must knowfreq 44%

basics

~20 s

Verify builds chains from that certificate to a root in opts.Roots, using opts.Intermediates as available links, and checks signatures, validity dates, CA constraints, extended key usage and, when DNSName is set, the hostname. It never checks revocation.

open as a page

Verify on a customer's PEM chain returns 'certificate signed by unknown authority' — how do you find the missing link?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Split the bundle into PEM blocks, parse each, and print Subject, Issuer, NotAfter and DNSNames. The break is where a certificate's Issuer matches no other's Subject and no trusted root; Go never fetches a missing issuer.

open as a page

What does the bool returned by x509.CertPool.AppendCertsFromPEM actually tell you?

level: middleimportance: nice to knowfreq 31%

basics

~20 s

It reports whether at least one certificate in the PEM input was parsed and added, not whether all were. A bundle where most blocks are corrupt still returns true, so it is not a validity check.

open as a page