Skip to content

feat: verify rfc8785 jcs bundle integrals (payloadVersion 3) - #3

Open
emdin wants to merge 2 commits into
igra-canonical-exit-toccatafrom
igra-bundle-jcs-payload-v3
Open

emdin wants to merge 2 commits into
igra-canonical-exit-toccatafrom
igra-bundle-jcs-payload-v3

Conversation

@emdin

@emdin emdin commented Sep 26, 2026 •

Copy link
Copy Markdown

Summary

cast igra verify-bundle-integral rejects manifests whose bundleIdentity.payloadVersion is 3, because their integral payload is canonicalized with RFC 8785 JCS (rfc8785-jcs-utf8):

Error: unsupported bundleIntegral.canonicalization: rfc8785-jcs-utf8

This adds JCS support and hardens how the verifier reads manifests. No signing, building or broadcasting code is touched.

JCS support

  • Object keys are ordered by UTF-16 code units (RFC 8785 section 3.2.3). Strings reuse serde_json escaping, which matches section 3.2.2.2.
  • Numbers are limited to integers with |n| <= 2^53 - 1, whose ES6 form is the plain decimal. Anything else is refused rather than hashed in a form the producer may not have used.

payloadVersion selects the canonicalization

  • 1 and 2, or no bundleIdentity: the legacy canonicalization, unchanged. 3: JCS.
  • A declared canonicalization that does not match, an unknown payloadVersion, or a bundleIdentity that is not an object is refused instead of falling back to the legacy canonicalization.

Strict manifest parsing

verify-bundle-integral hashes serde_json's view of the manifest, which can differ from what other JSON parsers read. The new parse_bundle_manifest refuses:

  • Duplicate object keys. serde_json keeps the last one, while other parsers may keep the first, and RFC 8785 requires I-JSON, which forbids them. Before this change, a legacy manifest with a conflicting duplicate "kind" key injected at the start verified as ok.
  • Keys starting with $serde_json::private::. With the arbitrary_precision and raw_value features enabled in this workspace, serde_json reads such an object as a number or as embedded JSON.
  • Numbers that are not 64-bit integers. serde_json passes them through the same reserved key, and manifests only carry integers.

Test plan

  • 14 unit tests in igra_bundle cover:
    • the RFC 8785 section 3.2.3 sorting example and the section 3.2.2 string-escaping example;
    • a synthetic JCS manifest whose expected hash comes from an independent Python RFC 8785 implementation;
    • tamper detection and the number subset;
    • payloadVersion pairing and unknown versions;
    • duplicate, reserved and escaped keys, non-integer numbers, and a round trip.
  • Mutation check: each new guard was removed in turn, and its test failed.
  • cargo test --locked -p foundry-common --lib: 99 passed, 2 ignored.
  • cargo clippy --all-targets on foundry-common (all features) and cast reports no findings in the changed files.
  • The changed code is clean under nightly rustfmt.
  • End to end with cargo build --locked -p cast --no-default-features, running verify-bundle-integral on every locally available bundle manifest:
    • every legacy manifest verifies, with output byte-identical to the previous binary;
    • every payloadVersion 3 manifest verifies, where the previous binary rejected it;
    • hostile copies are rejected: duplicate key, reserved key, non-integer number, tampered hash, unknown payloadVersion, and canonicalization downgrade.

CI failures that already exist on the base branch and are not addressed here:

  • cargo deny (dependency advisories and licenses);
  • clippy -Dwarnings findings in other files of both crates;
  • rustfmt differences in other code, including import grouping and doc comments already present in crates/cast/src/cmd/igra.rs.

🤖 Generated with Claude Code

emdin and others added 2 commits September 26, 2026 12:31
Bundle manifests with bundleIdentity.payloadVersion 3 canonicalize the
integral payload with RFC 8785 JCS (`rfc8785-jcs-utf8`). verify-bundle-integral
rejected them with "unsupported bundleIntegral.canonicalization".

- Accept `rfc8785-jcs-utf8` next to the existing canonicalization, which is
  unchanged.
- JCS orders object keys by UTF-16 code units and reuses serde_json string
  escaping, which matches RFC 8785 section 3.2.2.2.
- Numbers are limited to integers with |n| <= 2^53 - 1, whose ES6 form is the
  plain decimal. Anything else is refused rather than hashed in a form the
  producer may not have used.
- Require payloadVersion 3 for JCS and reject payloadVersion 3 with the
  legacy canonicalization, so a manifest cannot mix the two.

Tests use the RFC 8785 section 3.2.3 sorting example and a synthetic manifest
whose expected hash comes from an independent Python JCS implementation.
Existing payloadVersion 1 and 2 manifests verify unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
verify-bundle-integral hashes serde_json's view of a manifest, which can
differ from what other JSON parsers read:

- Duplicate object keys: serde_json keeps the last one, other parsers may
  keep the first. A legacy manifest with a conflicting duplicate "kind"
  key injected in front verified as ok before this change.
- Keys starting with `$serde_json::private::`: with the arbitrary_precision
  and raw_value features enabled in this workspace, serde_json reads such
  an object as a number or as embedded JSON.

parse_bundle_manifest refuses both, along with numbers that are not 64-bit
integers, which serde_json passes through the same reserved key. RFC 8785
requires I-JSON input, which forbids duplicate keys anyway.

The bundle-identity payloadVersion now selects the canonicalization:
1 and 2 (or no bundleIdentity) use the legacy one, 3 uses JCS. Any other
payloadVersion, or a bundleIdentity that is not an object, is refused
instead of falling back to the legacy canonicalization.

Also:
- collapse a nested if in jcs_number (clippy::collapsible_if)
- test the RFC 8785 section 3.2.2 string-escaping example
- document the supported canonicalizations in the bundle operations doc

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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.

1 participant