skip to content

What are the rules for a certificate template's SerialNumber in Go, and how should you generate one?

level: middleimportance: nice to knowfreq 26%

answer

  1. no default; the call fails without it
  2. it is an arbitrary-precision integer type
  3. positive, and capped by the spec
  4. uniqueness is scoped to one issuer
  5. randomness removes the need for shared state

basics

~10 s

SerialNumber is a *big.Int with no default: x509.CreateCertificate fails if it is nil. It must be positive, at most 20 octets, and unique per issuer. Generate 128 random bits with crypto/rand.Int rather than counting.

solid answer

~40 s

The field is `SerialNumber *big.Int` on `x509.Certificate`, and it is one of the very few the issuing call refuses to guess for you — leave it nil and `CreateCertificate` returns an error. The standards constraints are that the value is a positive integer of at most 20 octets, and that it is unique among all certificates that issuer has ever signed, because revocation identifies a certificate by the pair (issuer, serial). The idiom is a random 128-bit value: `rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))` using `crypto/rand`. Randomness buys two things: uniqueness without any coordination between replicas of an issuing service, and unpredictability, which historically mattered because a predictable serial made chosen-prefix collision attacks on weak signature hashes easier.

code

go · 6 lines
go
limit := new(big.Int).Lsh(big.NewInt(1), 128) // 2^128
serial, err := rand.Int(rand.Reader, limit)   // uniform in [0, 2^128)
if err != nil {
	return err
}
tmpl := x509.Certificate{SerialNumber: serial}

go deeper

for a junior

Know that the serial is a *big.Int you must set yourself, that leaving it nil makes issuance fail, and that the accepted way to produce one is random bytes from crypto/rand rather than a number you pick.

for a middle

Explain the constraints — positive, bounded in size, unique per issuer — and why randomness is preferred to a counter: no shared state between replicas and no disclosure of issuance order or volume.

for a senior

Talk about the serial as an operational handle: what your issuing service records against it, how revocation and audit refer to a certificate by issuer plus serial, and how that holds up when lifetimes are minutes and volume is high.

for a principal

Decide what your platform must be able to answer about any certificate it ever issued, and what retention and indexing that implies — the serial is the join key that makes an incident answerable rather than archaeological.

## The one field with no sensible default Most of `x509.Certificate` can be left zero and you still get a certificate — a bad one, perhaps, but a certificate. `SerialNumber` is different. It is declared as a `*big.Int`, and `x509.CreateCertificate` refuses to proceed when it is nil, returning an error instead. That is deliberate: the standard library will not invent an identifier whose uniqueness only the operator can guarantee. ## What the serial has to satisfy **Positive.** X.509 serials are signed integers in DER, and the specification requires a positive value. Zero and negative values are non-conforming; some verifiers reject them outright, and a negative value is usually the result of feeding raw random bytes into `big.Int.SetBytes` incorrectly or reusing a signed counter. **At most 20 octets.** That is the specification's ceiling, which is why 128 bits (16 octets, or 17 once DER prepends a zero byte to keep the top-bit-set value positive) is the standard choice and why nobody generates 256-bit serials. **Unique per issuer.** Uniqueness is scoped to the issuer, not to the universe. Two unrelated CAs may both issue serial 1 and nothing is broken, because everything that refers to a certificate by serial — a revocation list, an audit log, an operator tracing which certificate was presented — also names the issuer. Uniqueness within an issuer is what makes that reference unambiguous. ## Why random and not a counter A counter is legal. Plenty of CAs used one. It has two costs an internal issuing service feels immediately. The first is coordination. The moment the service runs as more than one replica, a counter needs shared, durable, transactional state — a row you increment, or a leased block of numbers. That is a real dependency, and it fails at exactly the wrong time: if the store is unavailable you cannot issue, and if two replicas ever disagree you emit duplicate serials that break revocation. A 128-bit random value needs no coordination at all; the probability that two replicas collide is negligible for any realistic issuance volume. The second is predictability. Sequential serials disclose issuance volume and ordering, and — the historical reason the industry moved — a predictable serial removes one obstacle to forging a certificate through a signature-hash collision. Modern hashes make that attack impractical, but the requirement for entropy in the serial is now baked into public-CA rules and is simply the expected practice. ## Generating one correctly `crypto/rand.Int(rand.Reader, max)` returns a uniform value in the half-open interval [0, max). With `max` set to two to the power of 128 you get a 128-bit serial. Two details: - Use `crypto/rand`, never `math/rand`. `math/rand` is a deterministic pseudo-random generator; a serial from it is predictable and, worse, may be identical across freshly started processes if the source is seeded identically. - The result can in principle be zero, which is not a conforming serial. The probability is one in two to the power of 128, so most code ignores it; a strict issuer adds one, or rejects and retries, and says so in a comment. ## Operational habits worth having An internal CA should record every serial it issues alongside the subject and validity window, even though it does not need that record to generate the next one. When an incident asks "which certificate is this, who asked for it, and is it still live", the serial is the join key, and reconstructing it later from scattered logs is miserable. This is also the field that makes short-lived certificates auditable: with fifteen-minute lifetimes you may issue millions, and the serial plus issuer is the only stable handle on any single one of them.

  • Why 128 bits rather than 64 or 256?
    The specification caps a serial at 20 octets, so 256 bits is out of range once DER encoding is accounted for. Sixty-four bits already meets the common entropy requirement, but 128 makes accidental collision across an issuer's whole lifetime unthinkable while staying comfortably inside the size limit. It is the boring, universally accepted choice.
  • Can two different CAs issue certificates with the same serial number?
    Yes, and it is harmless. Uniqueness is required only within a single issuer, because every reference to a certificate — revocation entries, audit records, transparency logs — carries the issuer name alongside the serial. The pair is what identifies a certificate, not the number on its own.
  • What happens if you leave SerialNumber nil in the template?
    The issuing call fails with an error rather than picking a value for you. It is one of the few fields the standard library refuses to default, because only the operator knows what has already been issued under that issuer name.

saying these in an interview costs you the question

  • Uses an incrementing counter across multiple issuer replicas
  • Leaves SerialNumber nil and expects a default
  • Uses math/rand to generate the serial
  • Thinks serials must be globally unique across all CAs
  • Builds the serial from a timestamp, making it predictable and collision-prone