Skip to content

fix(core): reject trailing bytes in manual decoders (#450)#455

Merged
cryptskii merged 2 commits into
mainfrom
fix/decoder-trailing-byte-rejection
Jun 5, 2026
Merged

fix(core): reject trailing bytes in manual decoders (#450)#455
cryptskii merged 2 commits into
mainfrom
fix/decoder-trailing-byte-rejection

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

What

Closes #450. Three hand-rolled decoders parsed a valid prefix and returned success while silently ignoring
trailing bytes, breaking canonicalization: two distinct wire byte strings could decode to the same value,
enabling signature/representation malleability (a signature verified over a re-derived canonical form would
accept both encodings).

How

Require full byte exhaustion in each decoder:

  • Operation::from_bytes — returns Err if any input remains after a complete parse.
  • SerializableMerkleProof::from_bytes — returns None unless the cursor consumed the entire buffer.
  • SmtInclusionProof::from_bytes — returns None unless offset + count * 32 == data.len().

Safety audit

Swept every from_bytes/decode in dsm/src and every call site of the three decoders across dsm,
dsm_sdk, the JNI bridge, and storage:

  • All callers pass exactly-sized buffers (protobuf bytes fields, to_bytes() output, or length-prefixed
    reads), so enforcing exhaustion breaks no caller. Sub-operation decodes are length-delimited, so no inner
    exhaustion check fires.
  • All other hand-rolled decoders already exhaust (spv, device_tree, kyber, capsule, smt_replace_witness,
    GenesisHash) or are protobuf/fixed-width (tombstone, policy).
  • SmtInclusionProof::from_bytes has no production callers — only unit + integration tests, both passing
    exact to_bytes() output.

Tests / verification

  • New trailing-byte rejection tests for all three decoders.
  • cargo test -p dsm --lib — 1437 passed.
  • cargo test -p dsm_sdk --lib (single-threaded) — 1506 passed.
  • cargo test -p dsm --test smt_tripwire_vectors — 25 passed.
  • cargo clippy -p dsm -p dsm_sdk --all-features — clean.

Related

cryptskii added 2 commits June 3, 2026 13:53
Operation::from_bytes and SerializableMerkleProof::from_bytes parsed a valid
prefix and returned success while silently ignoring trailing bytes. That broke
canonicalization: two distinct wire byte strings could decode to the same
value, enabling signature/representation malleability (a signature verified
over a re-derived canonical form would accept both encodings).

Require full byte exhaustion: Operation::from_bytes now returns Err if any
input remains after a complete parse; SerializableMerkleProof::from_bytes
returns None unless the cursor consumed the entire buffer.

Audited every call site of both decoders across dsm, dsm_sdk, the JNI bridge,
and storage: all pass exactly-sized buffers (protobuf bytes fields, to_bytes()
output, or length-prefixed reads), so enforcing exhaustion breaks no caller.
Sub-operation decodes are length-delimited, so no inner exhaustion check fires.

Tests: trailing-byte rejection for Operation (Transfer with signature, and
Create) and for SerializableMerkleProof. Full dsm (1436) and dsm_sdk (1506)
lib suites pass; clippy clean.
Third hand-rolled decoder with the same gap: it length-checked each field but
never required full byte exhaustion, so trailing bytes were silently dropped.
Add an `offset + count * 32 != data.len()` check before constructing the proof.

Swept every from_bytes/decode in dsm/src: all others already exhaust (Operation
and SerializableMerkleProof fixed in the prior commits; spv, device_tree, kyber,
capsule, smt_replace_witness, GenesisHash strict; tombstone/policy protobuf).
This decoder has no production callers — only the new unit test and the
smt_tripwire_vectors integration test, both passing exact `to_bytes()` output.

Tests: trailing-byte rejection for SmtInclusionProof. Full dsm lib suite (1437)
and smt_tripwire_vectors (25) pass; clippy clean.
@cryptskii cryptskii merged commit 22f76e8 into main Jun 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manual Operation::from_bytes decoder tolerates trailing bytes (signature malleability)

1 participant