Skip to content

feat(attestation): hash intent and evidence as JCS and bind them on verify - #247

Merged
hyperpolymath merged 5 commits into
mainfrom
claude/gallant-bell-4bo4oy-d-attestation-jcs
Oct 26, 2026
Merged

hyperpolymath merged 5 commits into
mainfrom
claude/gallant-bell-4bo4oy-d-attestation-jcs

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Stacked PR D of the estate-format series:

A, B, C and E are merged. This PR builds on crate::json from the JCS-reports PR.

What changes

  • JCS hashing. assail --attest hashes the intent and evidence records as their JCS (RFC 8785) bytes via the new crate::json::to_canonical_bytes. It previously hashed serde_json::to_vec struct order. The .attestation.json sidecar itself is now written as I-JSON/JCS (J-1).
  • Content binding on verify (a fix). attest verify used to recompute only chain_hash from the three stored digests, so the embedded intent and evidence could be edited after sealing without detection. It now rehashes both from the file and fails on a mismatch. It hashes the parsed JSON tree rather than the typed structs, so fields this build doesn't know about stay inside the hash.
  • hash_scheme on the seal (jcs-sha256). The scheme is prefixed into the chain hash: SHA-256(hash_scheme || intent_hash || evidence_hash || report_hash). Any Ed25519 signature therefore covers it too.
  • Fixed-width digests (Codex P1, fixed in d88ccd9). attest verify rejects any digest field that isn't exactly 64 lowercase hex characters. Without that check, an attacker could move the scheme name into intent_hash and drop hash_scheme. That reproduced the signed bytes on the legacy path, which skips the content rehash. With fixed widths, the legacy chain input (192 bytes) and the scheme-prefixed one (202 bytes) can't coincide.
  • Legacy sidecars. Sidecars with no hash_scheme deserialise as serde-json-sha256, use the old chain-hash formula, and verify as before, reported as Content: not rehashed (legacy hash scheme). An unknown scheme fails.

Validation

  • cargo fmt --check, plus cargo clippy --all-targets -D warnings both with --all-features and without: clean.
  • cargo test --all-features: all suites green after merging main (lib: 509 passed).
  • New tests in src/attestation/mod.rs:
    • A fresh sidecar verifies with content bound.
    • intent_hash equals SHA-256 of a hand-written JCS string.
    • Pretty-printing the sidecar keeps it valid.
    • Editing intent or evidence is detected.
    • A genuine legacy seal verifies but is unbound.
    • Relabelling a seal as legacy breaks the chain hash.
    • The scheme-shift attack is rejected. That test reported no failures before d88ccd9.
    • An unknown scheme fails.
  • End-to-end with the built binary, cross-checked with tools that share no code with the crate:
    • jq -cjS .attestation.intent | sha256sum and the same for evidence match the seal.
    • sha256sum report.json matches report_hash.
    • printf 'jcs-sha256%s%s%s' I E R | sha256sum matches chain_hash.
    • Editing the evidence fails verification.
    • Downgrade plus edit fails with a chain mismatch.
    • On a signed sidecar, the signature verifies, a downgrade fails, and the scheme-shift attack fails (intent_hash is not a SHA-256 digest).

Proof angle

src/abi/AttestationUnforgeability.idr is parametric in chainHash under the chainCR (injectivity) hypothesis. Its doc now states the scope: the model covers one scheme at a time, and integrity, authenticity and nonRepudiation hold per scheme.

Scheme binding (no downgrade) is not modelled. In the code it rests on the fixed-width digest check. The Codex P1 was a failure of exactly the injectivity chainCR assumes: the bug was in the encoding, not in SHA-256.

The model can be extended with a fourth chainHash input and a no-downgrade lemma. That needs an Idris2 type-check, which this environment couldn't run, and no workflow here type-checks src/abi/. The open review thread on seal.rs holds that decision for the owner.

Follow-ups (outside this PR)

  • attest verify --report <file> to recompute report_hash from the report itself; today it is only cross-checked against decision_hash.
  • A --require-bound mode (or a policy default) that rejects legacy, unbound seals.
  • An Idris2 type-check job for src/abi/ in CI.
  • evidence.evidence_hash hashes usize::to_le_bytes, whose width depends on the platform. That is harmless for the seal, which uses JCS, but not portable.
  • Consumers that parse the sidecar (DYADT / K9 contractiles) should read hash_scheme. That work is in other repositories.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k

…erify

Seals now hash the intent and evidence records as their JCS (RFC 8785)
bytes, so anyone holding the sidecar can recompute them in any language
(jq -cjS ... | sha256sum). The sidecar itself is written as I-JSON/JCS.

`attest verify` now rehashes the embedded intent and evidence. Before,
it only checked the chain hash against the three stored digests, so
both records could be edited after sealing without detection.

The seal records a `hash_scheme` (`jcs-sha256`), prefixed into the
chain hash so any Ed25519 signature covers it: dropping the field to
skip the content check breaks the chain hash. Sidecars from before the
field verify as legacy, reported as not rehashed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath marked this pull request as ready for review October 26, 2026 19:29
@hyperpolymath
hyperpolymath enabled auto-merge (squash) October 26, 2026 19:29
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-26T21:23:12.027970Z 661cdd9 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2770d5755

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/attestation/seal.rs
hasher.update(report_hash.as_bytes());
hex::encode(hasher.finalize())
};
let chain_hash = chain_hash(HASH_SCHEME_JCS, &intent_hash, &evidence_hash, &report_hash);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update the formal model for the hash-scheme input

This changes the signed chain digest to include hash_scheme, but src/abi/AttestationUnforgeability.idr:7-12,44-67 still claims to mirror this implementation while modeling chainHash as a function of only intent, evidence, and report. Consequently, the proof no longer covers the new security-critical property that changing the scheme invalidates the seal. Add the scheme as a modeled input and update the associated integrity/collision statements so the formal attestation guarantee matches this implementation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Partly addressed in d88ccd9. The model's doc now states the scheme-prefixed formula and its scope. chainHash is the hash for one fixed scheme, so integrity, authenticity and nonRepudiation hold per scheme. Scheme binding (no downgrade) is explicitly listed as not modelled, and the doc says which verifier check it rests on.

I haven't changed the proof terms. Adding a fourth chainHash input plus a no-downgrade lemma needs an Idris2 type-check, which I couldn't run in this environment, and no workflow here type-checks src/abi/. I've left this thread open for the owner to decide whether that lands in this PR or as a follow-up.


Generated by Claude Code

Comment thread src/attestation/seal.rs
@coderabbitai

coderabbitai Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Add Carrot credits or activate Agent usage billing to use Autofix

The legacy chain hashes `i || e || r` and the JCS chain hashes
`"jcs-sha256" || i || e || r`. With variable-width digest fields, moving
the scheme name into `intent_hash` and dropping `hash_scheme` reproduced
the signed bytes exactly while falling back to the legacy path, which
skips the content rehash. A signed sidecar could then be edited and still
verify. (Codex review finding on #247.)

`attest verify` now requires every digest to be 64 lowercase hex
characters, so the two chain inputs (192 and 202 bytes) cannot coincide.
A test reproduces the attack; it verified as OK before this change.

The Idris2 model's doc now states the scheme-prefixed formula and that
it covers one scheme at a time; scheme binding is not yet modelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
hyperpolymath added a commit that referenced this pull request Oct 26, 2026
Stacked PR E of the estate-format series:

- A: `.kyaml` loaders
- B: example profiles
- C: JCS reports
- C2: deterministic reports (#245)
- D: JCS attestation (#247)

This PR is independent of #245 and #247 and branches from `main`.

## What changes

- **New module `src/kyaml.rs`.** A KYAML emitter that follows mikefarah
`yq` v4's KYAML encoder (`pkg/yqlib/encoder_kyaml.go`, v4.54.1): flow
style, two-space indent, a trailing comma on every entry, bare keys only
for `^[A-Za-z_][A-Za-z0-9_-]*$`, and every string double-quoted.
- **Same data as the JCS report.** Keys follow JCS order (UTF-16 code
units) and numbers are in JCS form, so the KYAML and JSON reports of a
scan differ only in syntax.
- **New format value.** `ReportOutputFormat::Kyaml` adds
`--output-format kyaml`, extension `.kyaml`, parsed from `"kyaml"`.
Loaders have read `.kyaml` since PR A.
- **README fix.** `README.adoc` listed `--output-format` values
`markdown` and `text`, which never existed. It now lists the real ones.

## Contract, checked against the real tools

| Check | Result |
|---|---|
| Fixed point of `yq -p yaml -o kyaml`, the test
`scripts/kyaml-format.sh --check` runs | ✅ on all reports below |
| standards `scripts/kyaml-format.sh --check`, run for real | ✅ `all 4
file(s) are KYAML` |
| Byte-identical to `yq -p json -o kyaml` of the JCS report | ✅ two
historical `reports/*.yaml` assault reports, plus assail of `src/` (52
KB) and `tests/` |
| Read back through `crate::yaml` (serde-saphyr) | ✅ same data as the
JCS report |

The test `real_report_is_byte_identical_to_yq` pins this in CI without
yq. `tests/fixtures/kyaml/assail-report.kyaml` is yq's output for the
checked-in JCS report next to it.

## Two documented differences from yq

1. **Non-printables.** YAML's non-printables (U+007F–U+009F, U+FFFE,
U+FFFF) are escaped as `\uXXXX`. yq writes them raw, which YAML 1.2 does
not allow. A raw **U+0085 (NEL)** reads back as a line break, so yq
turns `"x\u0085y"` into `"x y"`. When a file contains one,
`kyaml-format.sh` reports `REFUSED … the KYAML rendering does not parse
to the same data` (exit 2). The gate therefore already treats yq's
output there as untrustworthy. I chose data integrity over gate
cleanliness for this rare case.
- **Alternative:** write them raw like yq. The gate stays quiet, but NEL
is silently corrupted.
- **Root-cause fix (beyond this repo):** escape these in yq's
`escapeDoubleQuotedString` upstream.
2. **Floats read from JSON.** `yq -p json` reformats some floats (`1e-7`
becomes `1e-07`, Go style). The KYAML report keeps the JCS form
(`1e-7`), which `yq -p yaml` preserves, so the fixed point holds. Byte
identity with yq's JSON→KYAML path holds whenever yq doesn't reformat,
which covers every report checked.

## Validation

- `cargo fmt --check`: clean.
- `cargo clippy --all-targets -D warnings`, with and without
`--all-features`: clean.
- `cargo test --all-features`: all 20 suites green.
- 8 new unit tests in `kyaml::tests`:
  - yq layout (expected text from yq);
  - scalars and lists at the top level;
  - C0 escapes;
  - non-printable escapes, including NEL read back intact;
  - JCS key order with a supplementary-plane key;
  - JCS and I-JSON numbers;
  - the real-report golden;
  - a report round trip.
- `--output-format bogus` now lists `json, yaml, kyaml, nickel, sarif`.

## Follow-ups (outside this PR)

- `assail` ignores `--output-format` and always writes JCS JSON.
Honouring it means deciding which bytes the `--attest` seal hashes.
- **Upstream (mikefarah/yq):** escape C1, DEL and NEL in the KYAML
encoder.
- **standards:** `kyaml-format.sh` could explain its refusal for this
case.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k)_

Co-authored-by: Claude <noreply@anthropic.com>
claude and others added 3 commits October 26, 2026 19:58
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
@hyperpolymath
hyperpolymath merged commit 2285a77 into main Oct 26, 2026
33 of 34 checks passed
@hyperpolymath
hyperpolymath deleted the claude/gallant-bell-4bo4oy-d-attestation-jcs branch October 26, 2026 21:23
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.

2 participants