Skip to content

Expose the bounded envelope read to callers outside this crate #69

Description

@sehkone

Context

The envelope blocks a container's footer locates carry attacker-written lengths, and this crate already knows it. read_bounded_block (src/payload.rs:1525) says so in its own doc comment:

The length decides before the seek does, so a block the caller cannot use is never allocated. That matters because this runs ahead of any authentication: the length comes from a footer an attacker wrote, and validate_footer confines it only to the input's own size, which costs nothing to inflate in a sparse file.

That reasoning is sound and its mitigation is real — but it is reachable through exactly one path. read_package_container (:1595), which reads both blocks under an EnvelopeBounds, is pub(crate), and its only caller is verify_package, which needs a TrustSet and performs full verification.

Every public reader takes the unbounded path instead: open (:2066), open_path (:2117), open_package (:2147) and open_package_path (:2167) all reach read_envelope (:1511), which calls read_block (:2029) — and read_block does vec![0u8; len] with no bound beyond validate_footer's "inside the input's own size". So a sparse file whose footer advertises a multi-gigabyte signature or key_id block makes any caller of the public readers allocate it, before anything is authenticated.

That is not a live defect in this crate today, because nothing here calls the public readers on untrusted input. It becomes one the moment a consumer reads a container's signature metadata for reporting rather than for verification, which is precisely what aicers/bootler's bootler-release verify-manifest needs to do: report whether a payload is signed and under which key_id, with no trust set on hand — that tool runs in CI where no key exists. Its review caught the allocation and correctly refused to work around it downstream: a local footer parser there would fork the container format, which is this crate's to own.

Decision

Expose the bounded read that already exists, rather than adding a second reading path.

The mitigation, the bound constants and the EnvelopeBlock::WrongLength reporting are all written and tested; what is missing is only that a caller outside this crate cannot ask for them. A new parallel implementation would be the two-implementations-must-agree failure this file argues against elsewhere.

Scope

  • Give callers outside this crate a way to read a container's envelope metadata under the same EnvelopeBounds discipline verify_package uses, without a TrustSet and without performing verification. Whether that is read_package_container made pub with EnvelopeBounds and EnvelopeBlock exported, or a narrower entry point over them, is the implementer's call — but there must be exactly one bounded implementation, and it must be the one verify_package already goes through.
  • Export whatever types that entry point's signature and result mention, so a consumer can name them: at minimum the bound descriptor and the per-block outcome, including the WrongLength case that reports an out-of-bound length rather than reading it.
  • Publish the bound values the release format actually uses, so a consumer does not restate 64 on its own side. ED25519_SIGNATURE_LEN and KEY_ID_HEX_LEN are verify.rs-private today; the consumer needs the same numbers the verifier enforces, and a second copy of them is a drift the container format cannot detect.
  • Leave open, open_path, open_package and open_package_path as they are. They are the whole-container readers, their callers already hold the bytes they are about to install, and changing what they return is a breaking change this issue does not need. Document on them that they read envelope blocks unbounded and name the bounded entry point for a caller reading untrusted input for metadata alone.

Acceptance criteria

  • A caller outside this crate can read a container's signature and key_id metadata without a TrustSet, through an entry point that never allocates a block whose length is not the bound for it.
  • A container whose footer advertises an out-of-bound signature or key_id length is reported as such rather than read, and the reporting distinguishes absent, present, and wrong-length.
  • A crafted sparse input advertising a multi-gigabyte block is handled without allocating it: a test builds one and asserts the call returns the wrong-length outcome, with the assertion resting on the reported outcome rather than on wall-clock or memory measurement.
  • The bound values the verifier enforces are readable by a consumer, and a test asserts the exported values are the ones verify_package uses rather than a second copy.
  • verify_package still goes through the same bounded implementation; there is not a second one.
  • open, open_path, open_package and open_package_path keep their current signatures and behaviour, and their documentation names the bounded entry point and says plainly that they do not bound envelope blocks.

Constraints

  • Do not change the container format, the footer layout, the absence encoding, or FORMAT_VERSION. This issue changes visibility and documentation, not bytes on disk.
  • Do not add a second bounded reader, a second copy of the bound constants, or a configurable bound: the bounds are the release format's, not a caller's choice.
  • Do not make Footer or Footer::encode public. A consumer gets metadata, not the footer.
  • Do not add verification, trust-set handling, or signature checking to the new entry point. It reports what the container claims; deciding whether to believe it stays verify_package's.

Out of scope

  • Bounding the manifest or archive blocks. Their lengths are equally attacker-written, but their callers are installing the container's contents and already read them whole; changing that is a separate question with a different answer.
  • Any change in aicers/bootler, which adopts this after its pin moves.

Test plan

  • Unit-test the bounded entry point over an unsigned container, a correctly signed one, one with an out-of-bound signature length, one with an out-of-bound key_id length, and one with a half-zero pair.
  • Build a sparse-file fixture whose footer advertises a block far larger than the file's real contents and assert the wrong-length outcome without allocating it.
  • Assert the exported bound values equal the ones verify_package enforces.
  • cargo fmt -- --check --config group_imports=StdExternalCrate, cargo clippy --all-targets -- -D warnings, and cargo test including the test-support feature.

Dependencies

None. Everything this issue exposes is already implemented and tested on main.

The consumer is aicers/bootler#244, which reports a payload's signature state from bootler-release verify-manifest and will move its deploy-core pin onto this issue's merge commit.

Pointers

  • src/payload.rs:1525 (read_bounded_block, and the doc comment naming the attack), :1595 (read_package_container), :1511 (read_envelope), :2029 (read_block), :2066/:2117/:2147/:2167 (the public readers)
  • src/verify.rs:120 (ENVELOPE_BOUNDS), :102 (KEY_ID_HEX_LEN), :108 (ED25519_SIGNATURE_LEN)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions