Checksum-verify-on-deploy (X-Checksum-*)
The write-path integrity gate: when a client uploads an artifact and advertises its digest, Holger recomputes that digest over the bytes it actually received and rejects a mismatch before any backend sees the write. Holger's pitch is "the bytes are what you think they are"; this makes it true at the front door, not just at rest.
Why
Holger already guarantees integrity at rest (blake3 per-chunk verify on read, sha256 in the Cargo index, OCI digest verify) and on pull-through (upstream bytes are checked against the upstream's advertised sha256 before caching). The one hole was on deploy: a corrupted or truncated PUT — a flaky airgap USB copy, a half-written CI upload, a MITM on a mis-configured plaintext hop — was accepted as-is. Artifactory closes this with checksum-deploy: the client sends X-Checksum-Sha256 and the server verifies. Holger now does the same.
Contract
- Headers:
X-Checksum-Sha256and/orX-Checksum-Sha512, hex-encoded digest of the request body. Case-insensitive names (HTTP) and case-insensitive hex compare. - Opt-in: a missing or empty header is a no-op — the write proceeds exactly as before. This keeps every existing client working; verification is a guarantee you add, not a handshake you must perform.
- Strong digests only: sha256 and sha512.
sha1/md5are intentionally unsupported — a matching weak digest is not an integrity guarantee, and honouring them would invite a false sense of safety. (The workspace already standardises on sha256/blake3.) - On mismatch: the write is rejected with
400and asha256 checksum mismatch/sha512 checksum mismatchbody. The event is audited (ident, action, repo, path, peer,400, byte count, detail) via the sameAuditLogseam as every other write. - Ordering: the check runs after auth/RBAC and the read-only-repo gate, and after the body is collected (so the size cap still bounds memory), but before
handle_http2_requestdispatches to the backend. A rejected upload never reaches storage.
Where it lives
server/lib/src/exposed/http.rs:
verify_upload_checksum(want_sha256, want_sha512, body) -> Result<(), (u16, &str)>— the pure helper (no I/O), unit-tested in the same module.handle(...)captures the two header values beforereq.into_body()consumes the request, then calls the helper on the collected body just before dispatch.
Reuses the crate's existing sha2 + hex deps (already present for pull-through cache verification) — no new dependency.
Scope / non-goals
- HTTP native-protocol door only. The gRPC path (
grpc.rs) uploads typedArtifactId+ bytes; a gRPC checksum field is a follow-up (the proto would carry an optionalexpected_sha256). Noted, not built. - No checksum storage / sidecar synthesis (
.sha1/.md5files) — that is the Maven-metadata gap (feature-complete-gaps.md§1), separate work.
Tests
Eight unit tests in exposed::http::tests (checksum_*): absent header, empty header, sha256 match, uppercase-hex match, sha256 mismatch, garbage/short digest, sha512 match+mismatch, both-digests match. Pure-function coverage, no network — runs in the default cargo test -p holger-server-lib pass (the tests-in-code convention, matching the sibling rbac_* tests in the same module).