skip to content

What does x509.CreateCertificate return in Go, and what must you do before writing a .crt file?

level: juniorimportance: must knowfreq 40%

answer

  1. the function returns bytes, not a file
  2. binary first, text second
  3. encoding/pem does the wrapping
  4. the block Type string matters
  5. pub is the subject's, priv is the signer's

basics

~10 s

x509.CreateCertificate returns the signed certificate as raw DER bytes, not a file and not text. To get a .crt or .pem file, wrap those bytes with encoding/pem in a block whose Type is CERTIFICATE.

solid answer

~40 s

The signature is `CreateCertificate(rand io.Reader, template, parent *x509.Certificate, pub, priv any) ([]byte, error)`. The `[]byte` it returns is the DER encoding of the finished, signed certificate — binary ASN.1, not something you can `fmt.Println` usefully. Everything that reads certificates from disk in the Go world expects PEM, so you pass the DER to `pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: der})`, or `pem.EncodeToMemory` if you want the bytes back. The two other arguments people get wrong are `pub` and `priv`: `pub` is the *subject's* public key (the key that goes into the certificate) and `priv` is the *signer's* private key. The matching private key is written as a separate PEM file, typically `x509.MarshalPKCS8PrivateKey` under the block type `PRIVATE KEY`.

code

go · 6 lines
go
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
if err != nil {
	return err
}
// der is raw DER; a .crt file is that wrapped in PEM armour
return pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: der})

go deeper

for a junior

Remember the shape: fill a template, call x509.CreateCertificate, get DER bytes, wrap them with pem.Encode using the block type CERTIFICATE. Also remember that the public key argument belongs to the subject and the private key argument belongs to the issuer.

for a middle

Be ready to explain what DER and PEM actually are, why the ecosystem exchanges the text form, and how the private key is serialised and stored as a separate PKCS#8 PEM file rather than inside the certificate.

for a senior

Show that you treat the issued material as an artefact with a lifecycle: file permissions on the key, chain ordering when several blocks share a file, and a test that reads back what you wrote instead of trusting that the bytes were fine.

for a principal

Frame it as an interface question — what format your platform hands to consumers, whether keys are ever generated centrally or only ever on the machine that will use them, and what that choice costs you in audit and blast radius.

## The one function that issues a certificate In Go, minting a certificate is a single standard-library call: ``` func x509.CreateCertificate(rand io.Reader, template, parent *x509.Certificate, pub, priv any) ([]byte, error) ``` There is no builder, no config file and no side effect on disk. You hand it five things and it hands you bytes. - `rand` is a source of randomness for the signature; in practice always `crypto/rand.Reader`. - `template` is an `*x509.Certificate` you fill in yourself. It is used as a *description of what to issue* — serial number, subject, validity window, key usages, SAN entries — not as a parsed certificate. - `parent` is the issuing certificate. If you pass the same value you passed as `template`, the result is self-signed. - `pub` is the public key that will be embedded in the certificate — the key belonging to whoever the certificate is *about*. - `priv` is the private key that signs it — the key belonging to the *issuer*. Swapping `pub` and `priv` is the classic first-day mistake. When a root signs a leaf, `pub` is the leaf's public key and `priv` is the root's private key; they belong to two different key pairs, and nothing in the type signature stops you mixing them up because both parameters are `any`. ## DER, and why it is not the file you want The return value is **DER**: Distinguished Encoding Rules, the canonical binary form of the ASN.1 structure that an X.509 certificate is. It is a compact byte string. It is perfectly valid to write it straight to a `.der` or `.cer` file, and `x509.ParseCertificate` reads exactly this form. But nearly every tool, config file and Kubernetes-free deployment pipeline in the ecosystem exchanges certificates as **PEM**: the DER bytes base64-encoded, split into 64-character lines, and wrapped in `-----BEGIN CERTIFICATE-----` / `-----END CERTIFICATE-----` armour. PEM's advantage is that it is text — it survives environment variables, YAML, copy-paste and shell heredocs — and that several certificates can be concatenated into one file to ship a chain. The conversion lives in `encoding/pem`: ``` pem.Encode(w io.Writer, b *pem.Block) error pem.EncodeToMemory(b *pem.Block) []byte ``` A `pem.Block` has a `Type string`, optional `Headers map[string]string`, and `Bytes []byte`. For a certificate the type string is exactly `CERTIFICATE`. That label is not decoration: parsers switch on it, and a block labelled `PUBLIC KEY` or `CERTIFICATE REQUEST` holding certificate DER will be rejected by anything careful. ## The private key is a separate artefact `CreateCertificate` never touches the subject's private key — it only needs the public half. Whoever generated the key pair keeps the private key, and if the issuing service generated it on the requester's behalf it must serialise it separately: `x509.MarshalPKCS8PrivateKey(key)` produces DER for essentially any key type, and it goes in a PEM block of type `PRIVATE KEY`. (`x509.MarshalECPrivateKey` and the legacy `RSA PRIVATE KEY` block type still exist; PKCS#8 is the modern default.) A TLS server then loads the pair with `tls.X509KeyPair(certPEM, keyPEM)`, which is why the two files are conventionally kept side by side but never concatenated into one block. ## Errors you will actually hit `CreateCertificate` returns an error rather than panicking, and the most common one on a first attempt is a missing serial number: the template's `SerialNumber` field is a `*big.Int` with no default, and the call fails outright if it is nil. Others come from the key material — a `pub` that is not a supported public key type, or a `priv` whose type cannot sign. ## Shape of a correct issue-and-write Fill a template, call `CreateCertificate`, check the error, then `pem.Encode` the DER. Four steps, in that order. Two mistakes are worth naming: base64-encoding the DER yourself before handing it to `pem.Encode` (you get double-encoded garbage that decodes to nonsense), and naming a DER file `.pem` (nothing will read it, and the error messages are unhelpful because the parser sees no armour and gives up immediately).

  • What are the last two arguments to x509.CreateCertificate, and what breaks if you swap them?
    `pub` is the public key of the entity the certificate is about; `priv` is the private key of the issuer that signs it. Both are declared `any`, so a swap compiles. At best you get an error about an unsupported key type; at worst you issue a certificate that embeds the CA's public key and is signed by the leaf's key, which nothing in your chain will accept.
  • How do you write the matching private key alongside the certificate?
    Serialise it separately: `x509.MarshalPKCS8PrivateKey(key)` gives DER, which you wrap in a PEM block of type `PRIVATE KEY`. Keep it in its own file with restrictive permissions — `tls.X509KeyPair` takes the certificate PEM and the key PEM as two separate byte slices.
  • Can one PEM file hold more than one certificate?
    Yes. PEM blocks concatenate, so a leaf followed by its intermediates in one file is the normal way to ship a chain. Order matters for most consumers: leaf first, then each issuer upward. The root is usually omitted because the verifier is expected to already trust it.

saying these in an interview costs you the question

  • Thinks x509.CreateCertificate writes a file to disk
  • Base64-encodes the DER before handing it to pem.Encode
  • Passes the signer's private key where the subject's public key belongs
  • Names DER output .pem and wonders why parsers reject it
  • Puts certificate DER in a PEM block labelled PUBLIC KEY