#199: SHA-256 integrity hashes for attachments #259

Merged
fable-5 merged 1 commits from feat/199-attachment-integrity-hashes into main 2026-07-31 04:51:49 +02:00
Collaborator

Stacked on #258 (#202) -> #257 (#236). Issue #199.

  • attachments.sha256 (nullable) added by migration; upload computes the hash from the in-memory buffer being written (never re-reads disk).
  • Download reads and re-hashes the whole object BEFORE serving a byte (a stream cannot be un-sent; memory bounded by the max_file_bytes quota) and fails closed with attachment_integrity_failure (HTTP 500); the mismatch is audited as file.integrity_failed with both hashes. Null-hash rows (pre-backfill) are served unverified — the pre-#199 status quo.
  • Backfill rides the existing nightly orphan-file-sweep job (bounded batch, idempotent, no new scheduler job — job-count fence untouched); unreadable files are logged and retried, never silently skipped.
  • Tests (attachment-integrity.e2e.db.test.ts): upload stores the correct hash; on-disk tamper makes download fail closed with the code + audit row; backfill restores hashes, keeps unreadable rows null, and is idempotent. Full api suite green (476) against a fresh DB; migrate deploy verified on a second fresh DB.
  • Docs: security.md (behaviour + operator response), operations.md (backfill rider); plan checkbox. #229 note follows after merge.
Stacked on #258 (#202) -> #257 (#236). Issue #199. - `attachments.sha256` (nullable) added by migration; upload computes the hash from the in-memory buffer being written (never re-reads disk). - Download reads and re-hashes the whole object BEFORE serving a byte (a stream cannot be un-sent; memory bounded by the max_file_bytes quota) and fails closed with `attachment_integrity_failure` (HTTP 500); the mismatch is audited as `file.integrity_failed` with both hashes. Null-hash rows (pre-backfill) are served unverified — the pre-#199 status quo. - Backfill rides the existing nightly orphan-file-sweep job (bounded batch, idempotent, no new scheduler job — job-count fence untouched); unreadable files are logged and retried, never silently skipped. - Tests (`attachment-integrity.e2e.db.test.ts`): upload stores the correct hash; on-disk tamper makes download fail closed with the code + audit row; backfill restores hashes, keeps unreadable rows null, and is idempotent. Full api suite green (476) against a fresh DB; `migrate deploy` verified on a second fresh DB. - Docs: security.md (behaviour + operator response), operations.md (backfill rider); plan checkbox. #229 note follows after merge.
fable-5 added 4 commits 2026-07-31 04:32:42 +02:00
#236: pin the Node version
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m40s
CI / Build container images (pull_request) Successful in 4m15s
CI / Auth e2e pack (pull_request) Successful in 8m33s
CI / Import/export fidelity gate (pull_request) Successful in 59s
6a520e27b1
.node-version (22.15.1) becomes the single authoritative Node version:
CI/CD select Node only via node-version-file, every Dockerfile pins
node:22.15.1-alpine, and the engines floor in package.json states the
same version (open-ended upwards so a newer local Node keeps working —
reproducibility rests on images and CI). An early CI step fails on any
drift between those places; update procedure in operations.md
(Update strategy). Precondition for the reproducibility claim in #219.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
#236: also pin the node helper images in workflows
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 5m41s
CI / Build container images (pull_request) Successful in 3m9s
CI / Auth e2e pack (pull_request) Successful in 8m32s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Build and push images (push) Successful in 21s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 6m0s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m32s
CI / Import/export fidelity gate (push) Successful in 1m3s
9326177534
release.yml and drill.yml ran throwaway `docker run node:22.15-alpine`
helpers outside the pin; the drift check now also fails on any
node:<other>-alpine reference in .gitea/workflows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
#202: SBOM and license report in CI
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m33s
CI / Build container images (pull_request) Successful in 4m38s
CI / Auth e2e pack (pull_request) Successful in 9m14s
CI / Import/export fidelity gate (pull_request) Successful in 1m12s
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Deploy to Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Waiting to run
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
d3289b2167
The release run now generates CycloneDX 1.6 SBOMs with a pinned
anchore/syft container — one per released image (scanned from the
freshly built image tar, OS packages included) and one for the pnpm
workspace (from the lockfile) — plus the full pnpm licenses report, and
attaches everything as build artefacts BEFORE publishing the release,
so a red gate stops the release. Runner constraints dictated the
mechanics (documented in the workflow): the job talks to the HOST
daemon, so files travel into the syft container via docker cp and
images via docker save to a tar copied the same way (syft cannot read
a tar from stdin — verified).

scripts/check-licenses.mjs is the documented license policy: permissive
allowlist, MPL-2.0/CC-BY-4.0 with recorded reasoning, per-package
exception table (khroma: MIT text shipped, metadata missing). CI runs
the gate on every PR (pnpm licenses:check); positive and negative case
tested locally, both SBOM paths tested against real images/lockfile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
#199: SHA-256 integrity hashes for attachments
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m12s
CI / Build container images (pull_request) Successful in 3m4s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m2s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 29s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m35s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m10s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
74970f6073
Every upload stores the SHA-256 of its bytes, computed from the
in-memory buffer that is written — never by re-reading disk. Every
download re-hashes the stored object BEFORE the first byte leaves
(memory bounded by the max_file_bytes quota that gated the upload) and
fails closed on mismatch with attachment_integrity_failure; the
mismatch lands in the audit trail as file.integrity_failed with both
hashes. Detection of payload manipulation is the one integrity duty
par. 52 VSA leaves with the application — only it knows what the file
should be.

Pre-#199 rows are hashed by a bounded, idempotent backfill that rides
the existing nightly orphan-file-sweep job (no new scheduler job, job
fence untouched); unreadable files are logged and retried, never
silently skipped, and null-hash rows are served unverified only until
the backfill reaches them. Operator runbook note in security.md
(restore from backup, re-download, audit entry carries both hashes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
fable-5 merged commit 74970f6073 into main 2026-07-31 04:51:49 +02:00
Sign in to join this conversation.
No description provided.