In Go, how do you turn the bytes of a PEM certificate file into an *x509.Certificate?
answer
- two encodings, so two calls
- the outer layer is base64 text
- the inner payload is DER
- failure arrives as a nil block, not an error
- block.Bytes is what the parser wants
basics
~10 sCall 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 sIt 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 linesblock, 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 blockgo deeper
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.
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.
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.
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