ADR 0027 — Cross-language signing contract (Go ↔ Node byte-identical canonical encoding)¶
- Status: Accepted (formalized 2026-05-12 with v3.7.0 — Go + Node verifiers byte-match in CI)
- Date: 2026-05-11 (locked), 2026-05-12 (formalized)
- Deciders: TLSStress.Art project
- Targets: v3.7.0 (RSA-4096-PSS-SHA-384 real, ML-DSA-65 stub); Go 1.27 (ML-DSA-65 stdlib)
- Patent claim family: claim #18 — cross-language canonical encoding + dual-signature envelope (anchor for ZTP-prem camada 1)
- Umbrella ADR: 0026
Context¶
ZTP-prem camada 1 places licence-envelope signing in a Cloud HSM operator's environment: keys never leave the cloud, the operator on the customer's premises cannot mint a valid licence. The customer's bench needs to verify that the licence presented by the dashboard was signed in the cloud, with two cryptographic materials simultaneously (RSA-4096-PSS-SHA-384 today, ML-DSA-65 post-quantum tomorrow), and that the verification works without trusting the bench-local Go or Node runtime to canonicalise the JSON the same way.
Standard JSON Marshal is a non-canonical encoder — key order,
whitespace, integer rendering, and Unicode normalisation differ
between languages and even between minor versions of the same
language. A signature computed in Go on serialised bytes
will not re-verify in Node unless both sides reach an identical
intermediate byte buffer first. That's not a hypothetical: every
prior project that tried "we'll just sign the JSON" eventually
shipped a silent rotation-day outage when one side bumped its
parser.
Patent #18 anchors the specific canonical encoding + dual-algorithm envelope as the cross-language contract.
Decision¶
Adopt a canonical encoding contract with three properties:
- Sorted-keys at every object depth — recursive
- Byte-stable primitive rendering — integers as decimal, no superfluous trailing zeros, no Unicode normalisation surprises
- Single intermediate buffer (no streaming) — both sides produce one byte slice of identical length, with identical SHA-256
Implementation pair:
- Signer (Go) —
pkg/ztp-prem-signctl/canonical.goruns in the trusted Cloud HSM environment. Produces 295-byte canonical form for the reference envelope; signs it with RSA-4096-PSS-SHA-384 (real today) + ML-DSA-65 (stub until Go 1.27 shipscrypto/mldsastdlib). - Verifier (Node) —
dashboard/src/lib/license/envelope.tsruns on the customer's bench. Produces the same 295-byte canonical form; verifies the dual signature.
Cross-language byte-match assertion is a first-class CI gate.
pkg/ztp-prem-signctl/canonical_test.go reads a fixture envelope,
canonicalises it Go-side, and asserts the SHA-256 matches the
Node-side hash that the dashboard test suite produced in a prior
CI run. A drift on either side breaks the gate.
Consequences¶
Pros - Customer can verify offline; no Cloud HSM call needed at boot - Dual signature gives PQC migration headroom before RSA-4096 is broken (post-quantum compliance is now a recurring procurement question) - Single byte-stable contract means licence rotation is a signature-file swap, not a code release
Cons
- New tool (ztp-signctl) is moat-closed (Tier B) — garbled in
CI, never ships to customer DC
- Canonical encoder MUST be hand-maintained in two languages — a
third language (Rust agent? Python automation?) adds a
third copy
- ML-DSA-65 stub means today's PQC posture is "we will when stdlib
lands"; the contract is in place, the algorithm isn't yet
Reversibility: very low. Once envelopes are minted with this
canonical form, every existing licence in customer hands depends
on it. Replacing the canonical encoding would require a forced
re-issue across the customer base. Algorithm swaps inside the
envelope (e.g. ML-DSA-65 → ML-DSA-87 future) are reversible because
the envelope is a JSON object with a signatures[] array.
Related¶
pkg/ztp-prem-signctl/— Go signerdashboard/src/lib/license/envelope.ts— Node verifierplatform/ztp-prem/CLOUD-HSM-KEY-CUSTODY.md— key custody flow- ADR 0026 — ZTP-prem umbrella
- ADR 0033 — UTXO token vault + MÓDULO LICENSE.Art envelope
Last verified against shipping code: v3.7.0 (2026-05-12).