You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(storage): hash pins on S3/GCS via object metadata (#952)
Signed reland of #947 (by @jgrund).
Records the SHA-256 integrity pin as object metadata (x-amz-meta-sha256 / x-goog-meta-sha256) on the S3 and GCS backends, written atomically with the object so bytes are never stored unpinned; get re-verifies the pin on read and fails closed on a mismatch. Raw uploads are validated against an RFC 9530 Repr-Digest header when present. Also adds a direct validate_storage_key() at the top of Storage::get() for defense in depth.
main requires verified signatures and the original commits were unsigned, so this re-lands the reviewed diff as one web-flow-signed commit.
Co-authored-by: Joe Grund <grundjoseph@gmail.com>
Closes#947
Copy file name to clipboardExpand all lines: CHANGELOG.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,8 +1,9 @@
1
1
# Changelog
2
2
## [Unreleased]
3
3
4
-
### Fixed
5
-
-**GC: tag-rooted mark walk kept a tag manifest's children but swept the digest-named copy of the manifest itself**, so pull-by-digest of a tagged image 404'd after the first GC run while pull-by-tag kept working. The walk now marks `manifests/sha256:<sha256(bytes)>.json` for every tag manifest — the digest alias the OCI distribution spec requires to stay pullable. Orphaned digest manifests now also take their `.meta.json` sidecar with them instead of leaking it. (#949)
4
+
### Added
5
+
-**Hash pins on S3/GCS via object metadata** — the SHA-256 integrity pin is no longer a local-filesystem-only feature. On object-store backends it is written as the user-defined `sha256` object metadata, atomically with the object, and read back on GET/HEAD, so buffered reads verify at rest and raw files get `ETag`, `If-None-Match` (304) and `If-Match` conditional overwrite on every backend. Pins are now a backend concern: the local backend keeps its NDJSON sidecar (same path and format, no migration), the object-store backend keeps object metadata, and the storage wrapper only validates keys and runs the fail-closed verify gate. Objects written before the upgrade carry no metadata and stay open-world until they are rewritten; `nora re-pin` rewrites the object on an object store, since object metadata cannot be changed in place.
6
+
-**Raw upload integrity via `Repr-Digest` (RFC 9530)** — a raw `PUT` may declare `Repr-Digest: sha-256=:BASE64:`; NORA verifies the received body against it before committing, so a corrupted or truncated upload is rejected with `400` instead of being pinned. The pin itself is always the server-computed hash; the header only gates the commit. A `Repr-Digest` without a sha-256 entry is rejected rather than silently skipped.
Copy file name to clipboardExpand all lines: COMPAT.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -389,7 +389,7 @@ Helm charts are stored as OCI artifacts via the Docker registry endpoints. `helm
389
389
| Health check | Full |`/health`|
390
390
| Swagger/OpenAPI | Full |`/api-docs`|
391
391
| S3 backend | Full | AWS S3, Ceph RGW. Basic storage works on any S3-compatible; multi-replica write-serialization has a caveat — see note below. |
392
-
| GCS backend | Full | Native Google Cloud Storage (`storage.mode = "gcs"`): Workload Identity / service-account JSON / ambient credentials; endpoint override for emulators and Private Google Access. Same single-writer caveat as S3 for rpm/deb publishing (in-process publish lock). Hash-pinning (at-rest integrity verification) is unavailable on ALL object-store backends, not only S3. |
392
+
| GCS backend | Full | Native Google Cloud Storage (`storage.mode = "gcs"`): Workload Identity / service-account JSON / ambient credentials; endpoint override for emulators and Private Google Access. Same single-writer caveat as S3 for rpm/deb publishing (in-process publish lock). Hash-pinning (at-rest integrity verification) works on every backend: the pin is the `sha256` object metadata on S3/GCS. |
393
393
| Local filesystem backend | Full | Default, content-addressable |
394
394
| Activity log | Full | Recent push/pull in dashboard |
| Go Modules | ✅ | — |`proxy.golang.org`| proxy only (modules immutable, push not in protocol) |
40
-
| Raw files | ❌ | ✅ | — (no upstream) | hosted only; conditional `PUT` (ETag/`If-Match` — local backend only; `If-None-Match: *` works on any backend) |
40
+
| Raw files | ❌ | ✅ | — (no upstream) | hosted only; conditional `PUT`/`GET` (ETag, `If-Match`, `If-None-Match`) on every backend; upload verification via `Repr-Digest` (RFC 9530) |
41
41
| RubyGems | ✅ | ❌ |`rubygems.org`| proxy only — `gem push` not implemented in NORA v1.1.0 |
0 commit comments