Split canonicalization into a library of its own #1

Open
opened 2026-09-12 21:49:24 -05:00 by jeff · 0 comments
Owner

canonical.zig is useful to things that are not DKIM, and today the only way
to reach it is to depend on a crypto library.

The case that turned this up is zig-mime,
which now interoperates with this library over raw bytes — it builds a message,
zig-dkim signs it, and tests/dkim.zig over there checks that a message parsed
and written out again still verifies. That pairing needs nothing from this
issue. What it cannot do is produce a canonical form on its own, and the
three ways out of that are all bad:

  • zig-mime depends on zig-dkim, which puts a crypto library in the dependency
    tree of everything that wants to read mail;
  • zig-mime implements canonicalization itself, which is a second
    implementation of the same specification, and two implementations of one
    thing drift apart the first time either is touched;
  • nobody has it, which is where we are.

A library of its own fixes all three: one implementation, no crypto, and both
can use it.

What would move

src/canonical.zig entire — Canonicalization, Algorithms, body,
header, headerParts, Collector, Limited — plus the parts of
src/message.zig it rests on: split, Line, LineIterator, Header,
HeaderIterator, findFromBottom. That last group is arguably the more
reusable half: "where do the headers stop, and where does each field begin and
end" is a question every mail program asks, and this answer is allocation-free
and has been fuzzed.

zig-dkim would gain a dependency and re-export both under their present names,
so nothing downstream changes.

Why it is worth writing once

The parts that make canonicalization annoying are exactly the parts you do not
want implemented twice:

  • the empty body canonicalizing to "\r\n" under simple and "" under
    relaxed, so the two produce different bh= for the same empty message;
  • trailing empty lines, which cannot be written out until something non-empty
    turns up after them;
  • the l= interaction, where the limit applies to the canonicalized octets
    and not the raw ones;
  • the fixed-size sink design, which is what keeps the whole thing
    allocation-free.

Who else would want it

  • ARC (RFC 8617) uses the same two algorithms for AAR, AMS and AS.
  • Anything that wants a bh= without signing — a milter deciding whether a
    body changed in transit, a test fixture generator, a debugging tool.
  • zig-mime, for callers who want the canonical form of a message they are
    holding without taking on the crypto.

Not urgent

Nothing is blocked. zig-mime and zig-dkim compose over raw bytes today and
there is a test on the zig-mime side proving the round trip is good enough for
a signature to survive it. This is about making the canonical form reachable
without the crypto, and it can wait for a reason to exist.

`canonical.zig` is useful to things that are not DKIM, and today the only way to reach it is to depend on a crypto library. The case that turned this up is [zig-mime](https://git.jcollie.dev/jeff/zig-mime), which now interoperates with this library over raw bytes — it builds a message, zig-dkim signs it, and `tests/dkim.zig` over there checks that a message parsed and written out again still verifies. That pairing needs nothing from this issue. What it cannot do is produce a *canonical form* on its own, and the three ways out of that are all bad: * zig-mime depends on zig-dkim, which puts a crypto library in the dependency tree of everything that wants to read mail; * zig-mime implements canonicalization itself, which is a second implementation of the same specification, and two implementations of one thing drift apart the first time either is touched; * nobody has it, which is where we are. A library of its own fixes all three: one implementation, no crypto, and both can use it. ## What would move `src/canonical.zig` entire — `Canonicalization`, `Algorithms`, `body`, `header`, `headerParts`, `Collector`, `Limited` — plus the parts of `src/message.zig` it rests on: `split`, `Line`, `LineIterator`, `Header`, `HeaderIterator`, `findFromBottom`. That last group is arguably the more reusable half: "where do the headers stop, and where does each field begin and end" is a question every mail program asks, and this answer is allocation-free and has been fuzzed. zig-dkim would gain a dependency and re-export both under their present names, so nothing downstream changes. ## Why it is worth writing once The parts that make canonicalization annoying are exactly the parts you do not want implemented twice: * the empty body canonicalizing to `"\r\n"` under `simple` and `""` under `relaxed`, so the two produce different `bh=` for the same empty message; * trailing empty lines, which cannot be written out until something non-empty turns up after them; * the `l=` interaction, where the limit applies to the canonicalized octets and not the raw ones; * the fixed-size sink design, which is what keeps the whole thing allocation-free. ## Who else would want it * ARC (RFC 8617) uses the same two algorithms for `AAR`, `AMS` and `AS`. * Anything that wants a `bh=` without signing — a milter deciding whether a body changed in transit, a test fixture generator, a debugging tool. * zig-mime, for callers who want the canonical form of a message they are holding without taking on the crypto. ## Not urgent Nothing is blocked. zig-mime and zig-dkim compose over raw bytes today and there is a test on the zig-mime side proving the round trip is good enough for a signature to survive it. This is about making the canonical form reachable without the crypto, and it can wait for a reason to exist.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeff/zig-dkim#1
No description provided.