Skip to content

Expose the bounded container's manifest to callers outside the crate #72

Description

@sehkone

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 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.

pub fn parse_unverified_manifest(&self) -> Result<PayloadManifest, PayloadError>

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.
  • cargo build, cargo test, cargo fmt -- --check, cargo clippy --all-targets -- -D warnings and RUSTDOCFLAGS=-D warnings cargo doc --no-deps pass.

Dependencies

None. The bounded reader it extends landed in #69.

Pointers

  • src/payload.rs:1319UnparsedContainer and its five accessors, four of them pub(crate).
  • src/payload.rs:1617read_package_container, the bounded entry point this completes.
  • src/payload.rs:2105open's manifest parse and the ManifestError-to-PayloadError mapping to reproduce exactly.
  • src/manifest.rs:690PayloadManifest::parse, already pub, 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:120ENVELOPE_BOUNDS, the bounds an external caller passes.

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