You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
read_package_container (src/payload.rs:1617) is the bounded reader this crate exposes so a caller can read a container's envelope without letting an attacker-controlled footer length decide an allocation. It returns UnparsedContainer (src/payload.rs:1319), and that type hands out only two of the four things it holds:
accessor
line
visibility
signature()
1372
pub
key_id()
1379
pub
manifest_bytes()
1342
pub(crate)
footer_version()
1334
pub(crate)
into_payload()
1389
pub(crate)
So an external caller can reach the envelope blocks and nothing else. There is no route from a bounded read to the manifest — neither its bytes nor a parsed form — and the only public route to a parsed manifest is open, open_path, open_package and open_package_path, every one of which reads the envelope blocks with no bound. open_path's own doc comment says so and points at the bounded reader as the alternative.
That leaves a caller who wants manifest metadata under a bound with no option that is both correct and safe. It must open the container twice — once bounded to read the envelope, once unbounded to reach the manifest — which reopens exactly the unbounded allocation the bounded reader exists to close, and adds a time-of-check/time-of-use gap between the two opens over a file it does not control. aicers/bootler#244 is blocked on precisely this: its verify-manifest reporting path is required to read metadata through the bounded entry point and is forbidden from opening the container through open/open_path/open_package/open_package_path or parsing a footer of its own, and no combination of the current public API satisfies both.
The parse itself is already public: PayloadManifest::parse(manifest_bytes, footer_version) (src/manifest.rs:690). What is missing is the pair of inputs it takes, and the error mapping that turns its ManifestError into this module's PayloadError — which open performs at src/payload.rs:2105 and which is part of the contract rather than a caller's choice.
Scope
Add one public method to UnparsedContainer, and change nothing else about the read path.
It performs exactly what open performs at src/payload.rs:2105-2109: PayloadManifest::parse(self.manifest_bytes(), self.footer_version()), mapping ManifestError::Decode(source) to PayloadError::ManifestParse(source) and every other ManifestError to PayloadError::InvalidManifest(other). Keeping the mapping here rather than at each call site is the point: two callers that map it differently report different errors for the same container, and the mapping is a validation contract rather than a formatting choice.
The name states the trust status, because the API's shape cannot. This crate's own consumer authenticates before it parses — verify.rs reads the container at :846, calls verify_signature(&container, trust) at :849, and only then decides the version question, with the comment "Authenticate the raw manifest bytes before anything parses them." A public parse necessarily offers a caller the parse without that step, so the method is named for what it returns: a manifest decoded from bytes nothing has authenticated. A caller holding a TrustSet keeps using the verifying path; this entry point is for metadata reporting that runs with no key present at all, which is a real and supported mode rather than a shortcut.
manifest_bytes() and footer_version() stay pub(crate). Widening them instead was considered and is rejected. It reaches the same data in fewer lines, but it moves the ManifestError-to-PayloadError mapping to every caller, and it hands out the signed bytes with no accompanying way to parse them under this crate's rules — an invitation to serde_json::from_slice at the call site, which is the pre-versioned decode the two-stage parse exists to prevent (src/manifest.rs:690's doc records why the format version is decided before the typed decode).
Nothing about ordering, bounds or existing behaviour changes. The method reads state read_package_container already holds; it seeks nothing, allocates nothing beyond the parse, and cannot fail differently from open's parse of the same bytes.
Acceptance criteria
UnparsedContainer::parse_unverified_manifest is pub, takes &self, and returns Result<PayloadManifest, PayloadError>.
For any container, its Ok value equals the manifest open produces from the same bytes, and its Err value matches open's for the same failure — ManifestError::Decode as PayloadError::ManifestParse, every other ManifestError as PayloadError::InvalidManifest.
The method is reachable from outside the crate through payload::read_package_container alone, with no TrustSet, no key material and no second open of the source.
manifest_bytes(), footer_version() and into_payload() remain pub(crate); signature() and key_id() keep their current signatures.
The rustdoc states that the returned manifest is not authenticated, names verify.rs's authenticate-then-parse order as the pattern a caller holding a TrustSet follows instead, and says which supported mode this entry point serves.
No existing public signature, error variant, bound, or read order changes, and open, open_path, open_package and open_package_path are untouched.
Constraints
Do not seek the source or read further bytes: the method uses only what read_package_container already read.
Do not add a TrustSet parameter, a verification step, or a second bounds type. Authentication is the verifying path's, and this entry point must stay usable with no key present.
Do not relax EnvelopeBounds or change ENVELOPE_BOUNDS (src/verify.rs:120).
Do not duplicate PayloadManifest::parse or re-implement any part of the two-stage parse here.
Do not widen manifest_bytes(), footer_version() or into_payload().
Out of scope
Bounding the envelope reads inside open, open_path, open_package or open_package_path. Their unbounded behaviour is documented and their callers are unchanged by this.
Any change to signing, append_trailer, append_trailer_signed or rewrap_trailer.
Deprecating or removing the unbounded readers.
Test plan
A container written by append_trailer round-trips: read_package_container then parse_unverified_manifest yields a manifest equal to the one open returns for the same bytes.
The same equality holds for a container written by append_trailer_signed, with no TrustSet involved on the bounded path.
A container whose manifest block is not valid JSON reports PayloadError::ManifestParse from both entry points.
A container whose manifest carries an out-of-range format_version reports PayloadError::InvalidManifest from both entry points, distinct from the decode error above.
A container whose signature block is present at a length ENVELOPE_BOUNDS refuses still parses its manifest, proving the manifest path does not depend on a block the bounded read declined.
Context
read_package_container(src/payload.rs:1617) is the bounded reader this crate exposes so a caller can read a container's envelope without letting an attacker-controlled footer length decide an allocation. It returnsUnparsedContainer(src/payload.rs:1319), and that type hands out only two of the four things it holds:signature()pubkey_id()pubmanifest_bytes()pub(crate)footer_version()pub(crate)into_payload()pub(crate)So an external caller can reach the envelope blocks and nothing else. There is no route from a bounded read to the manifest — neither its bytes nor a parsed form — and the only public route to a parsed manifest is
open,open_path,open_packageandopen_package_path, every one of which reads the envelope blocks with no bound.open_path's own doc comment says so and points at the bounded reader as the alternative.That leaves a caller who wants manifest metadata under a bound with no option that is both correct and safe. It must open the container twice — once bounded to read the envelope, once unbounded to reach the manifest — which reopens exactly the unbounded allocation the bounded reader exists to close, and adds a time-of-check/time-of-use gap between the two opens over a file it does not control.
aicers/bootler#244is blocked on precisely this: itsverify-manifestreporting path is required to read metadata through the bounded entry point and is forbidden from opening the container throughopen/open_path/open_package/open_package_pathor parsing a footer of its own, and no combination of the current public API satisfies both.The parse itself is already public:
PayloadManifest::parse(manifest_bytes, footer_version)(src/manifest.rs:690). What is missing is the pair of inputs it takes, and the error mapping that turns itsManifestErrorinto this module'sPayloadError— whichopenperforms atsrc/payload.rs:2105and which is part of the contract rather than a caller's choice.Scope
Add one public method to
UnparsedContainer, and change nothing else about the read path.It performs exactly what
openperforms atsrc/payload.rs:2105-2109:PayloadManifest::parse(self.manifest_bytes(), self.footer_version()), mappingManifestError::Decode(source)toPayloadError::ManifestParse(source)and every otherManifestErrortoPayloadError::InvalidManifest(other). Keeping the mapping here rather than at each call site is the point: two callers that map it differently report different errors for the same container, and the mapping is a validation contract rather than a formatting choice.The name states the trust status, because the API's shape cannot. This crate's own consumer authenticates before it parses —
verify.rsreads the container at:846, callsverify_signature(&container, trust)at:849, and only then decides the version question, with the comment "Authenticate the raw manifest bytes before anything parses them." A public parse necessarily offers a caller the parse without that step, so the method is named for what it returns: a manifest decoded from bytes nothing has authenticated. A caller holding aTrustSetkeeps using the verifying path; this entry point is for metadata reporting that runs with no key present at all, which is a real and supported mode rather than a shortcut.manifest_bytes()andfooter_version()staypub(crate). Widening them instead was considered and is rejected. It reaches the same data in fewer lines, but it moves theManifestError-to-PayloadErrormapping to every caller, and it hands out the signed bytes with no accompanying way to parse them under this crate's rules — an invitation toserde_json::from_sliceat the call site, which is the pre-versioned decode the two-stage parse exists to prevent (src/manifest.rs:690's doc records why the format version is decided before the typed decode).Nothing about ordering, bounds or existing behaviour changes. The method reads state
read_package_containeralready holds; it seeks nothing, allocates nothing beyond the parse, and cannot fail differently fromopen's parse of the same bytes.Acceptance criteria
UnparsedContainer::parse_unverified_manifestispub, takes&self, and returnsResult<PayloadManifest, PayloadError>.Okvalue equals the manifestopenproduces from the same bytes, and itsErrvalue matchesopen's for the same failure —ManifestError::DecodeasPayloadError::ManifestParse, every otherManifestErrorasPayloadError::InvalidManifest.payload::read_package_containeralone, with noTrustSet, no key material and no second open of the source.manifest_bytes(),footer_version()andinto_payload()remainpub(crate);signature()andkey_id()keep their current signatures.verify.rs's authenticate-then-parse order as the pattern a caller holding aTrustSetfollows instead, and says which supported mode this entry point serves.open,open_path,open_packageandopen_package_pathare untouched.Constraints
read_package_containeralready read.TrustSetparameter, a verification step, or a second bounds type. Authentication is the verifying path's, and this entry point must stay usable with no key present.EnvelopeBoundsor changeENVELOPE_BOUNDS(src/verify.rs:120).PayloadManifest::parseor re-implement any part of the two-stage parse here.manifest_bytes(),footer_version()orinto_payload().Out of scope
open,open_path,open_packageoropen_package_path. Their unbounded behaviour is documented and their callers are unchanged by this.append_trailer,append_trailer_signedorrewrap_trailer.Test plan
append_trailerround-trips:read_package_containerthenparse_unverified_manifestyields a manifest equal to the oneopenreturns for the same bytes.append_trailer_signed, with noTrustSetinvolved on the bounded path.PayloadError::ManifestParsefrom both entry points.format_versionreportsPayloadError::InvalidManifestfrom both entry points, distinct from the decode error above.ENVELOPE_BOUNDSrefuses still parses its manifest, proving the manifest path does not depend on a block the bounded read declined.cargo build,cargo test,cargo fmt -- --check,cargo clippy --all-targets -- -D warningsandRUSTDOCFLAGS=-D warnings cargo doc --no-depspass.Dependencies
None. The bounded reader it extends landed in #69.
Pointers
src/payload.rs:1319—UnparsedContainerand its five accessors, four of thempub(crate).src/payload.rs:1617—read_package_container, the bounded entry point this completes.src/payload.rs:2105—open's manifest parse and theManifestError-to-PayloadErrormapping to reproduce exactly.src/manifest.rs:690—PayloadManifest::parse, alreadypub, and its doc on why the format version is read before the typed decode.src/verify.rs:843— the in-crate consumer's staged order: bounded read, authenticate the raw bytes, then decide the version.src/verify.rs:120—ENVELOPE_BOUNDS, the bounds an external caller passes.