The disk is lava: generate your test certs in memory
To picture a nanosecond, Grace Hopper handed out a foot of wire: the distance light moves in a billionth of a second. A microsecond was a thousand feet of it. Memory still lives in the wire; disk, even a fast NVMe, lives in the coil. Reading a file is a trip into that slower time order, and pretending it's free is how a test suite ends up IO-bound. A test that reads a certificate off disk pays that toll, and committed a private key to your repo to do it. Stop reading, start generating.
You're not testing the cipher
When a test needs a TLS cert it's tempting to treat it as precious and check it
in. But you're almost never testing "does P-256 signing work" or "is the
handshake correct": you trust crypto/tls and crypto/x509 for that, the same
way you trust the compiler. You're testing your logic: that your option
builder wires the cert into the transport, that your chain verification accepts
a good chain and rejects a broken one, that your load-cert-from-a-path API reads
the file.
The cert is a prop. Props don't belong in your repo.
Generate it on the fly
A self-signed cert is a dozen lines of standard library:
func selfSigned(t *testing.T) tls.Certificate {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour), // minted per run, can't rot
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IsCA: true,
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
require.NoError(t, err)
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
}
Validity is minted relative to time.Now() on every run, so it can't expire.
Signing a leaf with a CA is the same call with a different parent; a chain is
just Certificate: [][]byte{leaf, intermediate}.
And it's faster
Generating the cert beats reading a precreated one off disk:
Generating an ECDSA P-256 cert took ~63 µs; reading the same one off disk
took ~200 µs (means of 10 runs, variance under 5%). The generated path holds
the cert bytes and a live key, so there's nothing to parse. Reading from disk,
X509KeyPair has to parse the cert and the private key and check they match
(~13 µs), and that's on top of ~190 µs spent reading the files. Generation
skips both. Pure CPU.
And this is the best case for the disk. The numbers are from an M5 Max with about the fastest NVMe you can buy. Keygen is cheap because CPUs have had accelerated elliptic-curve math for years, and that file read will never be quicker than this. On a real CI runner the gap only widens: slower cores, an overlay filesystem, a cold cache, sometimes storage over the network.
This only holds because ECDSA P-256 keygen is microseconds. Generate RSA-2048 instead and it jumps to ~38 ms, 600× slower, paid on every test. Pick a cheap key; the tests care about chain verification, not key type.
The real win: nothing to keep
Speed is a bonus. The real reason is that nothing lingers:
- Nothing expires: no fixture quietly rotting on a date you forgot, no red build on a branch nobody touched.
- No secrets in the repo: A committed private key is bait for every secret
scanner you run (push protection, gitleaks, trufflehog), a steady drip of
alerts to triage. Generated keys never hit disk, except a throwaway
t.TempDir()when an API genuinely reads a path. - No fixture toolchain: No
generate.sh, no openssl, no tribal knowledge about who regenerates the certs and how. The trust relationships live in code, right next to the test that leans on them.
The certs are just the example. The rule underneath: CPU time is cheap, disk IO is precious. When you can burn cycles to skip a syscall, burn the cycles.