Skip to content

feat: add signed package manifest flow - #2287

Merged
degenaro merged 2 commits into
oscal-compass:developfrom
MatteoFari:feat/2037-signed-manifest
Jul 23, 2026
Merged

feat: add signed package manifest flow#2287
degenaro merged 2 commits into
oscal-compass:developfrom
MatteoFari:feat/2037-signed-manifest

Conversation

@MatteoFari

Copy link
Copy Markdown
Contributor

Types of changes

  • Hot fix (emergency fix and release)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Documentation (change which affects the documentation site)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Release (develop -> main)

Quality assurance (all should be covered).

  • My code follows the code style of this project.
  • Documentation for my change is up to date?
  • My PR meets testing requirements.
  • All new and existing tests passed.
  • All commits are signed-off.

Summary

Adds beta support for signing and verifying YAML-defined JSON package manifests.

This introduces trestle sign-manifest and trestle verify-manifest. The manifest explicitly lists package artifacts, trestle canonicalizes each listed JSON file, computes SHA-256 digests, builds an in-toto Statement containing all artifact subjects, and signs that Statement as a detached DSSE envelope.

This PR is digest-only package verification. It does not auto-discover OSCAL dependencies, enforce per-document .dsse signatures.

Key links:

Before you merge

  • Ensure it is a 'squash commit' if not a release.
  • Ensure CI is currently passing
  • Check sonar. If you are working for a fork a maintainer will reach out, if required.

@MatteoFari
MatteoFari requested a review from a team as a code owner July 9, 2026 08:35
@degenaro

degenaro commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

claude's review:

Review: feat: add signed package manifest flow (#2287)

Author: MatteoFari · Base: develop · Status: Open · Part of #2037 Files changed: 11 (3 docs, 8 Python) · Lines: +801 / −2

Summary

Adds a beta-gated trestle sign-manifest / trestle verify-manifest flow. Given a YAML manifest listing JSON package artifacts, trestle:

  1. Canonicalizes each listed JSON artifact per RFC 8785.
  2. Computes a SHA-256 digest for each.
  3. Builds an in-toto v1 Statement with one subject per artifact and a new oscal-package/v1 predicate (tool, manifest version, primary artifact, ordered artifact list).
  4. Signs the Statement as a detached DSSE envelope, reusing the existing trestle sign/verify DSSE machinery in signing.py.

This is explicitly scoped as digest-only package verification — no dependency auto-discovery, no per-document signature enforcement (noted as future work).

Structure

File Purpose
trestle/core/signing_manifest.py (new) Manifest loading/validation, SigningManifest/ManifestArtifact dataclasses, Statement building, verification logic
trestle/core/commands/sign_manifest.py, verify_manifest.py (new) CLI command classes, thin wrappers over the above
trestle/core/signing.py (extended) Reused sign_in_toto_statement, verify_dsse_payload, load_in_toto_statement, DSSE PAE helpers
trestle/core/beta_features.py New json-manifest-signing beta flag
trestle/cli.py Command registration
docs/predicates/oscal-package/v1.md, docs/tutorials/cli.md, docs/index.md Docs for the predicate schema and CLI usage
Two new test files (~740 lines) Unit tests for signing_manifest.py and CLI-level round-trip/tamper tests

Strengths

  • Good reuse of existing primitives. Rather than reinventing DSSE/PAE handling, this builds on the existing signing.py helpers (sign_in_toto_statement, verify_dsse_payload, dsse_pae), keeping the crypto path in one place.
  • Careful path handling. _artifact_path rejects absolute URIs and URIs with a scheme, and confirms the resolved path stays within the manifest's directory (relative_to check) — a sensible guard against path traversal via a malicious or malformed manifest.
  • Constant-time digest comparisons. Both signing.py and signing_manifest.py use hmac.compare_digest for digest/signature comparisons instead of ==.
  • Output-path safety in write_dsse_envelope. Refuses to overwrite an existing file, refuses a symlink target, and refuses when output path collides with the manifest or key path (checked in sign_manifest.py) — avoids silently clobbering artifacts.
  • Strict, closed-world schema validation. _reject_unknown_fields rejects unrecognized manifest/artifact keys rather than silently ignoring them, and duplicate artifact names/subject names are explicitly rejected.
  • Predicate equality check is exact-match, not subset. verify_manifest_envelope compares the entire predicate dict for equality, so an attacker can't smuggle in an extra field or omit primaryArtifact without detection.
  • Beta-gating is applied consistently via the existing @beta_feature decorator pattern, matching how trestle sign/verify are gated — no special-casing.
  • Decent test coverage across both the pure-logic layer (signing_manifest_test.py) and CLI/round-trip behavior (tampered envelope, wrong key, changed artifact, changed manifest, encrypted private key, beta-flag gating).

Points worth checking / discussing

  1. YAML loading uses YAML(typ='safe') — good, this avoids arbitrary object deserialization. Worth confirming ruamel's safe loader is pinned to a version where this guarantee holds, but this is a reasonable default.

  2. Artifact digesting re-parses and re-canonicalizes on every verify. _artifact_digest calls load_canonical_json_file per artifact per verification — fine functionally, but for large packages with many artifacts this means re-reading/re-serializing every file on each verify-manifest call. Not a correctness issue, just a scalability note if manifests grow large.

  3. No explicit artifact count / manifest size limit. Unlike signing.py's MAX_SIGNATURES = 16 cap on DSSE signatures, signing_manifest.py doesn't cap the number of artifacts in a manifest. Probably fine since this is a local CLI tool (not a network-facing verifier), but worth a thought if manifests could ever come from untrusted sources.

  4. primaryArtifact is recorded but not otherwise enforced. It's just a labeled field in the predicate — verification doesn't appear to treat the primary artifact any differently from the rest (e.g., no requirement that it be verified first or exclusively). That seems intentional given the "digest-only" scope, but the semantic meaning of "primary" beyond documentation purposes isn't obvious from the code — might be worth a one-line clarifying comment on intended future use.

  5. Predicate type URI is a GitHub Pages URL (https://oscal-compass.github.io/compliance-trestle/predicates/oscal-package/v1) that, per the docs, is "not fetched during verification" — good, this avoids a live network dependency during signature checks, and is called out explicitly in docs/predicates/oscal-package/v1.md.

  6. Minor: exception handling in _run methods is broad (except Exception as e: # pragma: no cover) — consistent with the existing sign/verify commands, so not a regression, just inherited style.

  7. Docs are clear and cross-linked (CLI tutorial ↔ predicate spec ↔ index), and the "Before you merge" checklist items are all ticked. The PR is explicitly scoped ("does not auto-discover... does not enforce per-document signatures") which sets good expectations for reviewers and future maintainers.

Overall

This is a focused, well-tested addition that mostly reuses proven code from the existing single-file signing path rather than introducing new cryptographic logic. The path-traversal guard, constant-time comparisons, and strict schema validation are the right defensive choices for a manifest format that could plausibly be hand-edited or come from a less-trusted source. No blocking issues found; the notes above are mostly scalability/clarity observations rather than correctness concerns.

@butler54 butler54 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overal it looks pretty solid to me. I've got one big question which is how we define the manifest as yaml vs json.

'--signature', help='Path to the detached DSSE package envelope.', required=True, type=pathlib.Path
)
self.add_argument(
'--key', help='Path to the PEM public key for verification.', required=True, type=pathlib.Path

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit - we have used key for the private key. Can we be consistent across all commands for public vs private key arguments?
e.g. this can become --public-key

# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Trestle Sign Manifest Command."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one meta discussion point that' I'm confused with - which is why are our manifests for our json artifacts yaml?

Also a formal schema for the manifest should exist. (if it's json that is easier).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initially I chose YAML because it's more readable, but if we’re aiming toward automatically generated manifests and using tooling rather than manual editing, JSON makes more sense.

Signed-off-by: Matteo Fari <matteofari06@gmail.com>
@MatteoFari

Copy link
Copy Markdown
Contributor Author

Switched package manifests from YAML to strict JSON, added a JSON Schema, renamed verification’s key option to --public-key.

@MatteoFari
MatteoFari force-pushed the feat/2037-signed-manifest branch from b8198dc to f3c1dcf Compare July 23, 2026 09:17
@degenaro
degenaro self-requested a review July 23, 2026 11:34

@degenaro degenaro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@degenaro
degenaro merged commit 187cc78 into oscal-compass:develop Jul 23, 2026
16 checks passed
@degenaro degenaro added the GSoC label Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants