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
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
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)
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: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 anEnvelopeBounds, ispub(crate), and its only caller isverify_package, which needs aTrustSetand performs full verification.Every public reader takes the unbounded path instead:
open(:2066),open_path(:2117),open_package(:2147) andopen_package_path(:2167) all reachread_envelope(:1511), which callsread_block(:2029) — andread_blockdoesvec![0u8; len]with no bound beyondvalidate_footer's "inside the input's own size". So a sparse file whose footer advertises a multi-gigabyte signature orkey_idblock 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'sbootler-release verify-manifestneeds to do: report whether a payload is signed and under whichkey_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::WrongLengthreporting 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
EnvelopeBoundsdisciplineverify_packageuses, without aTrustSetand without performing verification. Whether that isread_package_containermadepubwithEnvelopeBoundsandEnvelopeBlockexported, 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 oneverify_packagealready goes through.WrongLengthcase that reports an out-of-bound length rather than reading it.64on its own side.ED25519_SIGNATURE_LENandKEY_ID_HEX_LENareverify.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.open,open_path,open_packageandopen_package_pathas 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
key_idmetadata without aTrustSet, through an entry point that never allocates a block whose length is not the bound for it.key_idlength is reported as such rather than read, and the reporting distinguishes absent, present, and wrong-length.verify_packageuses rather than a second copy.verify_packagestill goes through the same bounded implementation; there is not a second one.open,open_path,open_packageandopen_package_pathkeep their current signatures and behaviour, and their documentation names the bounded entry point and says plainly that they do not bound envelope blocks.Constraints
FORMAT_VERSION. This issue changes visibility and documentation, not bytes on disk.FooterorFooter::encodepublic. A consumer gets metadata, not the footer.verify_package's.Out of scope
aicers/bootler, which adopts this after its pin moves.Test plan
key_idlength, and one with a half-zero pair.verify_packageenforces.cargo fmt -- --check --config group_imports=StdExternalCrate,cargo clippy --all-targets -- -D warnings, andcargo testincluding thetest-supportfeature.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 frombootler-release verify-manifestand will move itsdeploy-corepin 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)