Skip to content

feat(evidence)!: retain acceptance evidence in signed git, not S3 Object Lock - #314

Closed
abrichr wants to merge 1 commit into
mainfrom
feat/git-backed-retention
Closed

feat(evidence)!: retain acceptance evidence in signed git, not S3 Object Lock#314
abrichr wants to merge 1 commit into
mainfrom
feat/git-backed-retention

Conversation

@abrichr

@abrichr abrichr commented Aug 27, 2026

Copy link
Copy Markdown
Member

Replaces S3 Object Lock with a signed git commit as the retained-evidence store. Decided before the first write, which was the last moment it was free: a COMPLIANCE lock cannot be shortened by anyone, including the account root, so the first retained object would have committed storage for its full term.

Why S3 was weaker than it looked

I argued for Object Lock earlier in this thread and was wrong. Naming the attack rather than the feature list gives a different answer:

Attack What actually stops it
Run acceptance, it fails, never mention it Nothing in S3. The certificate sits unread in a private bucket. Publishing the digest at creation time is what helps, and Rekor already does that
Alter a published result The signature and the Rekor entry. Object Lock adds nothing
Destroy the payload after publishing Object Lock keeps bytes in your bucket, which only helps a third party who has access

The confidentiality argument also fails on inspection. encryptPrivateEnvelope in the Cloud writer already encrypts with an AES-256-GCM data key wrapped by KMS, so the store holds ciphertext. GitHub cannot read it. Confidentiality never depended on the medium.

And git is content-addressed: a commit hash covers the whole tree, so it binds the relationships between certificate, admission and campaign. A per-object lock binds each blob alone.

What Object Lock genuinely bought, and why we are giving it up

If a third party is granted direct read access to the store, they can trust nothing was pruned before they looked. That is real. No buyer has asked for it. The document records the trade so the next reader does not reconstruct it, and says to revisit before the next write if a buyer ever does.

The schema change

Removed Added
retention_mode retention_commit (exact 40-hex)
retention_until transparency_log_entry_sha256
object_version_sha256 push_verified
object_lock_verified commit_verified
upload_verified, head_verified transparency_logged

The contract destination becomes repository, ref, path_prefix, keeping account_id, region and kms_key_arn because the data key is still KMS-wrapped. storage_identity_sha256 now digests the repository instead of the bucket.

One thing deliberately not enforced

retention_commitment_days is recorded and not policed. A git commit has no expiry. Enforcing a number nothing can hold would be theatre, so verify_retention_against_contract does not check it and both the code comment and the document say so outright.

Verification

  • 359 tests pass.
  • New tests cover the exact commit format, retention dated before acceptance, retention dated in the future, and the three new verification booleans.
  • The template still refuses unfilled.
  • ruff check . passes.
  • The hashed policy digest moves with the medium: bc013d65…77130f49…, updated deliberately in the pinned assertion.

This does not ship alone

The Cloud writer still writes to S3. Neither side can run until both agree, and the Cloud repository is a different lane with its own frozen gate. This PR changes the verifier only; the gate is still closed, so nothing live depends on it. The Cloud change needs its owner's review before either merges in anger.

🤖 Generated with Claude Code

…ect Lock

Decided before the first write, which was the last moment it was free. A
COMPLIANCE lock cannot be shortened by anyone including the account root, so
the first retained object would have committed storage for its full term.

The case for S3 was weaker than it looked. The Cloud writer already encrypts
the envelope client-side with an AES-256-GCM data key wrapped by KMS, so the
store holds ciphertext and confidentiality never depended on the medium. What
makes the claim credible is the signature chain and the public Rekor entry:
Object Lock stops neither the suppression of an unpublished result nor the
alteration of a published one, because the signature and the transparency log
already do both. Git is content-addressed, so a commit hash binds the
relationships between certificate, admission and campaign rather than each blob
alone.

Object Lock does buy one thing git does not. If a third party is granted direct
read access to the store, they can trust nothing was pruned before they looked.
No buyer has asked for that. The document records the trade so the next reader
does not have to reconstruct it, and says to revisit before the next write if a
buyer ever does ask.

The retention block loses retention_mode, retention_until, object_version_sha256
and object_lock_verified. It gains retention_commit, transparency_log_entry_sha256,
push_verified, commit_verified and transparency_logged. The contract destination
becomes a repository, ref and path prefix, keeping the account, region and KMS
ARN because the data key is still wrapped by KMS.

retention_commitment_days is recorded and not enforced, and both the code and
the document say so. A git commit has no expiry, and policing a number nothing
can hold would be theatre.

The hashed policy digest moves with the medium: bc013d65... -> 77130f49...

The Cloud writer must change to match before either side can run. That is a
separate lane and a separate review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abrichr

abrichr commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

Closing this at 70ace5a because it removes the COMPLIANCE storage required by the qualification contract and replaces it with a mutable private Git ref. The importer accepts a commit SHA and self-asserted booleans, but it does not fetch and verify the commit, signature, ref ancestry, Rekor entry, or readback. It derives destination_approval_sha256 without enforcing the approved ref or path. This also overlaps #303 and conflicts with the signed source-manifest and storage-seal work now in progress.

The branch remains preserved. Keep the legacy importer until the replacement has enforceable retention, exact destination approval, verified commit and transparency identities, and terminal readback proof.

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