Saltar al contenido

Esta página todavía no está disponible en español. Estás viendo la versión en inglés.

Development

The ULID spec admits its own bit math doesn't add up, then never explains it

Por Victor Da Luz
securitytestingdev-logfreetoolbox

I built a UUID/ULID generator with a collision-probability calculator for freetoolbox.net, and along the way I ran into a spec that tells you exactly where it’s incomplete and then just… stops there.

ULID’s format is 26 characters of Crockford’s base32: a 10-character timestamp followed by a 16-character randomness block. 26 characters times 5 bits each is 130 bits of encoding capacity. But the data underneath is only 128 bits - a 48-bit millisecond timestamp plus 80 bits of randomness. The spec’s own README states this mismatch directly, in plain terms: 130 bits of character space for 128 bits of actual data, 2 bits wasted. What it does not do anywhere in its prose is explain how those characters map onto the two fields - which end is the timestamp, whether it’s big-endian or little-endian, whether the encoding is byte-wise or bit-wise across the 32-symbol alphabet.

That’s a strange thing for a spec to leave out. RFC 9562 (UUID) spells out every bit position in the version and variant fields down to which nibble carries which flag. TOTP and HOTP’s RFCs (I hit this same “the spec is silent, don’t trust a paraphrase of it” lesson on a different tool a session earlier) at least gave me byte-exact test vectors to check my implementation against, even when a bad fetch summary corrupted one in transit. ULID gives you the bit-count arithmetic and then hands you a canonical example string with no walkthrough of how to read it.

The example is 01ARZ3NDEKTSV4RRFFQ69G5FAV. I decided to treat it as ground truth rather than keep hunting for prose that wasn’t there. I took the first 10 characters, 01ARZ3NDEK, and decoded them as a plain base-32 integer, most-significant-symbol first, using Crockford’s alphabet. That produced 1469922850259. Converted from Unix milliseconds, that’s July 30, 2016, 23:54:10.259 UTC - a completely unremarkable, plausible date, not a garbage number or an epoch boundary. If I’d had the endianness or the field order backward, I’d have gotten either an error (index out of the alphabet) or a date so absurd it would’ve been obviously wrong - the far future, or 1970. Getting a sane date back was the confirmation signal. Only then did I write encodeTimestamp/decodeTimestamp against that layout and let the test suite pin the exact number.

The broader thing I’m noticing across the last few of these tools: specs are reliable for the values they choose to publish and silent or actively wrong about everything else, and those two failure modes need different defenses. A corrupted fetch of a real published value gets caught by re-deriving the number yourself from a real implementation. A spec that never published the value at all - ULID’s encoding order - can’t be re-derived from the spec’s own text no matter how carefully you read it; the only way through is finding one artifact the spec author actually committed to (their own worked example) and treating it as the test oracle. I didn’t have language for that distinction going into this issue. I do now.

Lecturas relacionadas

Development

When an RFC test vector isn't the RFC's test vector

A fetched table of RFC hex values looked fine at a glance. Several entries were one character short - a summarization artifact, not a spec error, and it would have shipped silently if I'd trusted it.

Leer
Development

El error que en realidad era el depurador

El error de recorte móvil en todo el sitio que no existía: Chrome headless limita en silencio a un piso de 500px y recorta la captura al ancho que se haya pedido.

Leer
Development

El bug detrás del bug

Un widget de GitHub roto llevaba a una demo del proyecto original en pausa, y de paso apareció una CSP fijada por hash invalidada en silencio por un arreglo de accesibilidad de una línea.

Leer

También te podría ser útil

Proton

Proton Drive

Almacenamiento en la nube cifrado, del equipo detrás de Proton Mail.

Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).

Más información
AdGuard

AdGuard para iOS

Bloqueo de anuncios y rastreadores en todo el sistema en iOS, sin necesidad de un servidor DNS aparte.

Como afiliado de AdGuard, obtengo ingresos por las compras que califican.

Más información
NordPass

NordPass

Gestor de contraseñas del equipo detrás de NordVPN, con un plan gratuito.

Como afiliado de NordPass, obtengo ingresos por las compras que califican.

Más información