Shayl.Taveras
portfolio / projects / evidence-chain-of-custody
← back to portfolio
// Project Walkthrough · 07
Evidence Management & Chain of Custody

Extended the CI policy gate with keyless Cosign signing over GitHub OIDC so every evidence bundle landing in the vault is cryptographically tied to the exact CI run that produced it. Object Lock proves evidence can't be deleted. This proves who put it there and when. An auditor verifies the chain independently without trusting the AWS account.

cosign sigstore github oidc aws s3 object lock · rekor · fulcio · nist 800-53 · fedramp moderate
Problem Statement
01

Object Lock proves evidence can't be deleted. It doesn't prove who put it there or when. A privileged account holder could drop a tampered bundle in under a different key and point a careless auditor at it. The object would pass an integrity check against the wrong hash, and nothing in Object Lock would flag it.

The gap is authenticity and non-repudiation. AU-10 requires that audit records be traceable to a specific identity and time. A signed S3 object with no provenance doesn't satisfy that. It satisfies immutability, which is a different control entirely. Cosign over GitHub OIDC closes that gap by binding every evidence bundle to the exact workflow, repository, and commit that produced it.

How It Closes the Gap
02

The CI job runs cosign sign-blob on the evidence bundle. Sigstore's Fulcio CA issues a short-lived certificate based on the GitHub Actions OIDC token. No private key ever exists to leak or rotate. The certificate's Subject Alternative Name encodes the exact repo and workflow. Signature, cert, and a Rekor transparency-log timestamp all pack into a single .sig.bundle file uploaded next to the bundle.

// fulcio CA
No Key to Leak or Rotate
Identity comes from the GitHub OIDC token scoped to the exact repo and workflow. The Fulcio cert's SAN encodes the subject directly. There is no private key in the repo, no secrets to manage, nothing to steal.
// rekor log
Public Transparency Log Entry
Every signing event gets a timestamped, append-only Rekor entry. An auditor can verify the signature against the Rekor log independently, without AWS account access or any trust relationship with the account owner.
// .sig.bundle
One File, Self-Contained
The bundle format packs signature, certificate, and Rekor reference into one file. Verification needs nothing else: no separate cert fetch, no key lookup, no external state.
// if: always()
Sign on Failure Too
The sign and upload step runs even when the gate fails. Proving the gate fired matters as much as proving it passed. A failed gate run with a signed evidence bundle is a stronger audit artifact than a passing run with none.
What verify-evidence.sh Proves
03

The verification script proves three things independently. An auditor runs it against any bundle in the vault and gets a pass or fail on each assertion. No interpretation required.

check 1 · integrity
Hash Matches the Sidecar
Recomputes SHA-256 of the downloaded bundle and compares it against the sidecar hash file. Any byte-level change to the bundle, including appending a single byte, produces a completely different hash. The tamper test confirmed this: the modified copy produced 0d9d3b1c... against the sidecar's d131c41e...
check 2 · authenticity + timestamp
Cosign Verifies Against Public OIDC Issuer
Cosign verifies the signature against the public GitHub OIDC issuer and confirms the Rekor transparency log entry. This proves the bundle was signed by a GitHub Actions workflow in the specified repository, not just that it was signed by someone with the right key.
check 3 · preservation
Object Lock Retention Still Active
Checks that S3 Object Lock's RetainUntilDate is still in the future. A tampered copy can never overwrite the real object because Object Lock blocks rewrites until retention expires. The combination of integrity, authenticity, and preservation together is what AU-9(3) and AU-10 require.
// verify-evidence.sh: three independent checks
# 1. Integrity: recompute and compare hash
sha256sum evidence-bundle.zip | diff - evidence-bundle.zip.sha256

# 2. Authenticity: verify signature against Rekor + GitHub OIDC
cosign verify-blob evidence-bundle.zip \
  --bundle evidence-bundle.zip.sig.bundle \
  --certificate-identity-regexp "github.com/Shayl-Taveras" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com"

# 3. Preservation: confirm Object Lock is still active
aws s3api get-object-retention --bucket vault-bucket --key evidence-bundle.zip
The Tamper Test
04

Downloaded the real signed bundle from the vault, appended one byte, and recomputed the hash. The result was a clean mismatch: 0d9d3b1c... versus the sidecar's d131c41e.... The tampered copy couldn't be uploaded to overwrite the original because Object Lock blocked the write until retention expires.

// why it matters
Testing against a real downloaded vault object rather than a synthetic example proves detection works against actual bytes produced by the pipeline, not a toy scenario constructed to pass. An auditor reviewing this output can trust the tamper detection is real.
Key Technical Decisions
05
DecisionReasoning
Keyless OIDC signingIdentity scoped to the exact repo and workflow: no private key to leak, rotate, or steal. Fulcio cert SAN encodes the subject directly.
--bundle output formatOne file carries signature, cert, and Rekor reference. Verification needs nothing else.
if: always() on sign/uploadEvidence from a failed gate run still gets signed and preserved. Proving the gate fired matters as much as proving it passed.
Inline IAM policy scoped to one bucketThe CI role had only ReadOnlyAccess. New grant adds nothing beyond PutObject, GetObject, and GetBucketLocation on that one bucket.
GOVERNANCE retention, not COMPLIANCELab needs same-day cleanup. Production evidence uses COMPLIANCE, which nobody including root can bypass before expiry.
Real tamper test, not syntheticCorrupting an actual downloaded vault object proves detection against real bytes, not a constructed example.
File Structure
06
.github/workflows/
  policy-gate.yml # CI gate: includes cosign sign + upload steps
scripts/
  verify-evidence.sh # three-check verification script
evidence/ (S3 vault)
  evidence-bundle.zip # signed bundle: Object Lock protected
  evidence-bundle.zip.sha256 # SHA-256 sidecar
  evidence-bundle.zip.sig.bundle # cosign bundle: sig + cert + rekor ref
Summary
07
3
checks verified
4
controls covered
0
keys to manage
0
console logins needed
Controls
AU-9(3)
Crypto Protection of Audit Info
AU-10
Non-Repudiation
AU-11
Audit Retention
SC-13
Crypto Protection
Stack
signing
Cosign + Sigstore
identity
GitHub OIDC
CA
Fulcio
log
Rekor
storage
S3 + Object Lock
retention
GOVERNANCE
key management
none, keyless
Frameworks
NIST 800-53
Rev 5
FedRAMP
Moderate
Links
github
portfolio