AI Initiative logoAI Initiative
Reading room / Manthan

Audit D-316 — T-001 / T-003 cross-implementation interop bug

Audit D-316 — T-001 / T-003 cross-implementation interop bug. Covers TL;DR; Layer 1: domain-separation tag — stale `.v2` in T-001; Layer 2: nonce encoding; Layer 3: id / parent / trace encoding.

Historical audit / evidence record
This is the retained source text, not a new validation of its claims. Numerical results and comparative claims require the accompanying source review before public reuse.

Read the connected case study or article →

worker-glossary-2026-05-05 · 2026-05-05

Provenance, authorship and editorial notes

Audit D-316 — T-001 / T-003 cross-implementation interop bug

Id

manthan-herald-protocol-audits-t001-t003-interop

Project

Manthan

Source path

manthan/herald-protocol/audits/t001-t003-interop.md

Source sha256

42e6a803785fdb2af294f3bbbae00bc404f5df05352aabd108a674ebc24e2662

Local copy sha256

42e6a803785fdb2af294f3bbbae00bc404f5df05352aabd108a674ebc24e2662

Repo head

3a94084024a3434cef331fecac91c2c3b43f0313

Date

2026-05-05

Date basis

Explicit document metadata

Author

worker-glossary-2026-05-05

Author basis

Worker

Last source commit

8b75d1b0157c20a0a463f0e31008ef9f73099beb

Last source commit date

2026-06-08T07:40:14Z

Document status

Historical audit / evidence record

Summary

Audit D-316 — T-001 / T-003 cross-implementation interop bug. Covers TL;DR; Layer 1: domain-separation tag — stale `.v2` in T-001; Layer 2: nonce encoding; Layer 3: id / parent / trace encoding.

Topics
  • Adversarial review and case studies
  • TL;DR
  • Layer 1: domain-separation tag — stale `.v2` in T-001
  • Layer 2: nonce encoding
Theme

Adversarial review and case studies

Priority

reference

Related ids
  • manthan-herald-protocol-audits-manthan-cohort1-postmortem
Evidence concerns
  • Recovered source text is preserved verbatim. Assertions, comparisons, commands and results belong to its original context; they were not re-executed or externally re-verified during this corpus review.
  • Historical record: current deployment, task completion and test status must not be inferred from this date-bound document.
Word count

1537

Bytes

11232

Source relative

herald-protocol/audits/t001-t003-interop.md

Remote mirror

manthan/herald-protocol/audits/t001-t003-interop.md

Full text preserved

True

Audit D-316 — T-001 / T-003 cross-implementation interop bug

Worker: worker-glossary-2026-05-05
Date: 2026-05-05
Triggering finding: T-017 conformance runner (worker-glossary-par-8) classified 5 of 7 envelope-core test vectors as known_interop_gap rather than pass/fail. Root cause documented here in three layers.


TL;DR

When the T-017 runner exercises T-003's reference TypeScript library
against T-001's canonical test vectors, the two implementations produce
different signed bytes for the same logical envelope. There are
three independent root causes; SPEC is ambiguous on two of them and
stale on the third. Until SPEC picks, no two HERALD implementations
will interoperate.

#DefectT-001 choseT-003 choseSPEC says
1Origin domain-separation tagherald.envelope.v2herald.sig.origin.v1(post-D-308) herald.envelope.v1
2Encoding of nonce (16 bytes) in the canonical bodyCBOR byte string (major type 2)CBOR text string (major type 3, hex)silent
3Encoding of id, parent, trace (32-byte sha256s) in the canonical bodyCBOR byte string (major type 2)CBOR text string (major type 3, hex)silent

All three need closure. The Phase-1 reference deliverables (T-003,
T-004 Python, T-017 conformance, T-101 CT-log schema) all rest on this
choice.


Layer 1: domain-separation tag — stale .v2 in T-001

T-001 generate.py line 51 (manthan/state/findings/T-001/generate.py:51):

DOM_SEP_ENVELOPE = b"herald.envelope.v2"  # SPEC §3.6, origin scope, v2 tag

T-001 INTERPRETATION I-1 (manthan/state/findings/T-001/INTERPRETATION.md:9-29):

The task brief states ... dom_sep = "herald.envelope.v1". But the
current SPEC.md §3.6 lists ... herald.envelope.v2. ... We use
herald.envelope.v2.

That was correct as of T-001's submission timestamp. *D-308 has
since reverted SPEC.md §3.6 back to .v1
* (see
manthan/state/findings/D-308/RECEIPT.md). T-001's vectors are now
signed against a stale tag.

T-003 INTERPRETATION (manthan/state/findings/T-003/INTERPRETATION.md:11):

SPEC §3.6 line 212 lists herald.envelope.v1 for originating
envelopes. EXT §6.3 line 436 maps the origin signature scope to
herald.sig.origin.v1 ... this reference signs ordinary origin
signatures with herald.sig.origin.v1.

Note that T-003 uses herald.sig.origin.v1, not
herald.envelope.v1. EXT §13 line 770 says the two strings are
aliases for the origin scope:

herald.envelope.v1 | base envelope origin signature ([SPEC §3]) |
also herald.sig.origin.v1 for explicitly-scoped origin sigs

But "alias" at the documentation level does not mean the two
strings produce identical signatures. Ed25519 is signing
dom_sep || canonical_body, byte-for-byte. herald.envelope.v1 and
herald.sig.origin.v1 differ in 6 bytes; the resulting signatures are
different. *EXT §13 should pick one canonical string for the origin
scope and remove the alias language
*, or the registry should declare
them equivalent and require all implementations to sign with the same
string.

Recommended canonical answer: herald.sig.origin.v1 because EXT
§6.3 already maps the scope identifier origin → that tag, which is
the explicit signing-scope registry. SPEC §3.6's
herald.envelope.v1 should be marked as a synonym for "envelope
origin signature in SPEC's pre-multi-sig framing" and pointed at EXT
§6.3 / §13 as canonical. (Alternative: pick herald.envelope.v1
since SPEC §3.6 is normative; either way, pick once.)

Layer 2: nonce encoding

The 16-byte CSPRNG nonce per [SPEC §3.7] line 227 is opaque. SPEC §3.5
says nothing about how it is CBOR-encoded.

T-001 generate.py line 150 (manthan/state/findings/T-001/generate.py:150):

nonce: bytes,

— and at line 166–168 it lands in the body dict as bytes, which
goes through T-001's cbor_det() encoder as *major type 2 (byte
string)
*. The wire body for vector genesis-alpha-valid therefore
contains the nonce as a 17-byte CBOR atom (1-byte head + 16 bytes
payload).

T-003 envelope.ts line 95
(manthan/state/findings/T-003/src/envelope.ts:95):

nonce: nonceToHex(input.nonce),

— see nonceToHex() at line 191–193: returns a 32-character hex
string. This goes through T-003's encodeCbor() at
manthan/state/findings/T-003/src/cbor.ts:24 as *major type 3 (text
string)
*. The wire body contains the nonce as a 33-byte CBOR atom.

These produce different canonical bytes; therefore different ids;
therefore different signatures. The runner sees this as tampered
on every non-genesis vector at T-003-side verifyEnvelope line 114
(id mismatch).

SPEC §3.1's JSON example shows "nonce": "<128-bit CSPRNG random>"
which is just JSON's string-rendering of something — it does not
fix the CBOR-side encoding. SPEC §3.5 line 200 says "Map keys MUST be
text strings in v0.2"; values are unspecified.

Recommended canonical answer. CBOR byte string (major type 2).
Three reasons:

1. Compactness. A 16-byte nonce is 17 bytes as a byte string vs
33 bytes as hex text. For a 32-byte sha256 id, 33 vs 65. CBOR's
reason-for-being is compact wire format; choosing text-hex here
discards that benefit on every envelope.
2. Consistency with the CBOR-deterministic profile. SPEC §3.5
already requires byte-level canonicalisation; opaque values
should travel as bytes-typed atoms.
3. JSON display compat is preserved separately. The display-only
JSON form (SPEC §3.1) hex-encodes opaque values as strings; this
is purely a renderer decision and not load-bearing for signing.
T-003 conflates wire and display by storing hex in memory; the
fix is to keep wire types in memory (Uint8Array) and hex-encode
only at JSON-display boundaries.

Layer 3: id / parent / trace encoding

Same defect as Layer 2 but for the 32-byte sha256 hashes.

T-001 generate.py line 295 (and analogous sites):

parent=genesis_eid,    # bytes — CBOR major type 2 byte string

— with explicit comment confirming the choice. T-001 keeps id,
parent, trace as raw bytes throughout the canonical-body
construction.

T-003 envelope.ts line 158:

return bytesToHex(sha256(encodeCbor(body)));

— computeId returns hex out of bytesToHex. The hex string is
then stored on the envelope as id, and at line 92–93 parent and
trace are typed string. T-003's wire is text-string-typed for all
three.

Recommended canonical answer: same as Layer 2 — CBOR byte string
(major type 2) for id, parent, trace, nonce. Hex-text only at
the JSON display surface.


What SPEC needs to say

A small addition to SPEC §3.5 closes this. Proposed clause:

§3.5a Opaque-value encoding. The fields id, parent, trace,
and nonce are opaque byte sequences (id/parent/trace are
32-byte sha256 outputs; nonce is 16 bytes). They MUST be encoded
as CBOR byte strings (major type 2) in the canonical body. JSON
renderings (SPEC §3.1) display these values as lowercase hex text;
the JSON form is non-normative and not used for signing or hashing.

A second tightening for EXT §13:

Remove the "also herald.sig.origin.v1 for explicitly-scoped origin
sigs" alias language on the herald.envelope.v1 row. Make
herald.sig.origin.v1 the canonical origin-scope signing tag and
deprecate herald.envelope.v1 as a documentation synonym only.

(Or pick the other side. The point is to pick.)


What needs to happen after SPEC picks

1. T-001 vector regeneration. Once SPEC commits a domain tag and
an opaque-value encoding, regenerate test-vectors/v0.2/vectors.json
from generate.py. T-001's existing verify.py is internally
consistent with its generate.py, so they'll move together.
2. T-003 reconciliation. If SPEC adopts byte-string encoding (the
recommended answer), T-003 needs:
- Envelope.id, parent, trace, nonce typed Uint8Array on
the canonical-body interface (not string).
- nonceToHex removed from the build path; hex conversion lives
only at the JSON display boundary.
- Approximately one day of careful refactor + test re-derivation.
3. T-017 re-run. All 7 envelope-core vectors should pass
(modulo vector 7 lineage-cycle-birth which is an independent
issue around the missing rejected-lineage-cycle verifier verdict
per T-001 INTERPRETATION I-4).
4. Cross-language interop. T-004 (Python interop) is currently
blocked on T-003. With this reconciliation, T-004 can be
implemented against unambiguous SPEC text without re-discovering
the same encoding question.


What this audit does NOT do

promote).

main tree at test-vectors/v0.2/vectors.json; regenerating it
requires the SPEC decisions to land first.

a manthan submission still in findings/; it has not yet been
promoted. The reviewer who promotes T-003 should fold the Layer 2
+ Layer 3 fix into the promotion patch.

herald.sig.origin.v1. The audit recommends herald.sig.origin.v1
but flags both as defensible; protocol designers decide.

Verification

After the SPEC patches and the T-001 / T-003 reconciliations land,
running npm test from tests/conformance/ (T-017's deliverable)
should produce exit code 0 with all 7 envelope-core vectors passing
(or 6 + 1 skipped if vector 7 stays blocked on the
rejected-lineage-cycle verdict gap).

The T-019 glossary-check tool (tools/glossary-check.py) is silent on
this divergence — it catches lexical drift in spec prose, not byte-
level wire-format divergence. A future tool that diffs canonical-body
bytes between T-001 vectors and T-003-rebuilt envelopes would catch a
recurrence.


Cross-references

SourceCite
T-017 conformance runnermanthan/state/findings/T-017/RECEIPT.md (GAP A, GAP B, GAP C)
T-001 generate.pymanthan/state/findings/T-001/generate.py:51, :150, :166-168, :295
T-001 INTERPRETATIONmanthan/state/findings/T-001/INTERPRETATION.md:9-29 (I-1: domain tag), :75-101 (I-3: deterministic nonces)
T-003 envelope.tsmanthan/state/findings/T-003/src/envelope.ts:95, :114, :158, :191-193
T-003 cbor.tsmanthan/state/findings/T-003/src/cbor.ts:24-25
T-003 INTERPRETATIONmanthan/state/findings/T-003/INTERPRETATION.md:11 (origin domain tag choice)
D-308manthan/state/findings/D-308/RECEIPT.md (.v1 is canonical post-D-308)
SPEC §3.1SPEC.md:138-163 (JSON example, display-only)
SPEC §3.5SPEC.md:193-204 (canonicalisation profile, silent on opaque-value encoding)
SPEC §3.6SPEC.md:206-222 (domain-separation table, post-D-308 .v1)
SPEC §3.7SPEC.md:224-229 (nonce CSPRNG requirement)
EXT §13EXT.md:764-796 (domain-separation registry, alias language on origin row)

Continue exploring