Skip to content

fix(der): reject trailing data in der::decode - #562

Open
ivmat wants to merge 2 commits into
librasn:mainfrom
ivmat:fix-552-exact-consumption
Open

ivmat wants to merge 2 commits into
librasn:mainfrom
ivmat:fix-552-exact-consumption

Conversation

@ivmat

@ivmat ivmat commented Jul 15, 2026

Copy link
Copy Markdown

Summary

Fixes #552.

der::decode decoded the root value and returned without checking that the whole input
was consumed, so der::decode::<()>(&[0x05, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]) (a NULL
followed by four garbage bytes) decoded to () and silently dropped the trailing bytes.
X.690 §8.1.1.1 requires a DER message to be exactly one complete value with no trailing
data. The same leniency is what produced the round-trip divergence @tynus2 flagged, where
encode(decode(bytes)) can be shorter than bytes. I've kept out of the
security-classification discussion and scoped this purely to the exact-consumption / API
contract of decode<T>.

What the fix does

src/der.rs: after decoding the root value, der::decode now checks the decoder's
remaining input and returns DecodeErrorKind::UnexpectedExtraData { length } when any
bytes are left. It reuses the extra-data error rasn already emits for trailing bytes inside
a constructed value (src/ber/de.rs), so the behaviour and error type stay consistent with
the rest of the codec. decode_with_remainder is unchanged, for callers that intentionally
decode a value from the front of a larger buffer, and the decode doc comment now states
the strictness.

Scope: DER only, and the codec-wide question

I scoped this to der::decode, matching the issue. @Nicceboy's point that the same pattern
applies to several decode entry points is right: each codec has its own thin top-level
decode wrapper (ber, cer, oer, uper, ...), and the strictness lives in that
wrapper rather than in shared machinery, so extending it is mechanical. I deliberately did
not touch the others here:

  • BER has historically dropped trailing bytes (as you noted, established practice given its
    streaming heritage), so making it strict is a semantics decision I didn't want to take
    unilaterally.
  • CER is a distinguished form like DER and is a natural next candidate; OER/PER are the
    same shape of question.

If you'd like the same check applied uniformly across the strict decode entry points
(with the decode docs updated to state the strictness, per your suggestion), I'm happy to
do that as a follow-up — just say which codecs you want it on.

Real-world impact surfaced by the change

Two fixtures already in the tree carried trailing bytes that the lenient decoder silently
accepted, and both are legitimate:

  • standards/cms/tests/data/pesig.p7 (Authenticode): the PKCS#7 blob lives inside a
    WIN_CERTIFICATE structure padded to an 8-byte boundary, so it has two 00 padding bytes
    after the DER value. test_authenticode now parses it with decode_with_remainder.
  • standards/pkix/tests/data/splice.bin (a fuzz-regression fixture): a valid
    AlgorithmIdentifier followed by three trailing bytes. The splice test's DER arm now
    uses decode_with_remainder, matching its existing BER/CER arms.

Both are small, mechanical migrations to the "decode one value, ignore the rest" API, and
are included here so CI stays green. They're also a useful signal for the codec-wide
decision: trailing bytes do turn up in real inputs (container padding), so the strict
default and the decode_with_remainder escape hatch matter together.

Tests

tests/issue552.rs:

  • der_decode_rejects_trailing_bytes — the issue's exact NULL + 4 trailing bytes now
    returns UnexpectedExtraData { length: 4 }.
  • der_decode_rejects_trailing_bytes_on_integerINTEGER + 1 trailing byte.
  • der_decode_accepts_exact_input — exact encodings still decode.
  • der_decode_round_trip_holds_for_exact_inputencode(decode(bytes)) == bytes for
    exact input.

I confirmed the two rejection tests fail on the unpatched tree and pass with the fix. The
full workspace suite is green (cargo test --workspace), as are cargo fmt --all -- --check, cargo clippy --workspace --all-targets --features=f32,f64,bytes,std,backtraces -- -D warnings, and the doc build with -D warnings.

Evidence note (independent reference)

As an independent cross-check on the property, not as a dependency: I maintain
der-verified (0.1.0, MIT/Apache-2.0, proofs re-runnable from a fresh clone), a DER/X.690
core in which exact whole-input consumption is one of the verified properties. Its
top-level strict decoders return a distinct TrailingData error on any trailing bytes, and
a Kani harness proves that whenever the TLV reader accepts an input it consumes exactly
header + declared_length bytes and never over-reads. That is bounded model checking (a
16-byte symbolic buffer, loop unwinding to depth 16) rather than an all-length statement,
but it covers the trailing-data property on that domain; the crate also carries round-trip
harnesses of the same shape. This PR simply brings der::decode in line with that
exact-consumption behaviour.

ivmat added 2 commits July 15, 2026 18:07
The Authenticode PKCS#7 blob (a WIN_CERTIFICATE payload padded to an
8-byte boundary) and the pkix `splice` fuzz fixture both carry trailing
bytes after their DER value. Decode them with `decode_with_remainder` so
they keep working once `der::decode` rejects trailing data (see librasn#552).
`der::decode` decoded the root value and returned without checking that
the whole input was consumed, so e.g.
`der::decode::<()>(&[0x05, 0x00, 0xDE, 0xAD, 0xBE, 0xEF])` silently
accepted four trailing bytes. X.690 §8.1.1.1 requires a DER message to be
exactly one complete value with no trailing data.

Return `UnexpectedExtraData` when any bytes remain after the root value.
`decode_with_remainder` is unchanged for callers that decode a value from
the front of a larger buffer. Reported in librasn#552.
@ivmat

ivmat commented Jul 15, 2026

Copy link
Copy Markdown
Author

The matrix failures are pre-existing: current stable clippy's new byte_char_slices lint fires in src/types/strings/{numeric,printable}.rs, which this PR doesn't touch — main's last green CI run (May 4) predates the lint. Split the fix out into #563.

@XAMPPRocky

Copy link
Copy Markdown
Collaborator

should be fixed by rebasing now :)

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.

This can be an attack surface: der::decode() silently accepts trailing bytes — signature bypass vector

2 participants