Skip to content

Audit fixes: sorted fingerprints, lzma removal, sdist-true CI - 0.5.0.0 - #12

Merged
aoinoikaz merged 12 commits into
mainfrom
audit-2026-07
Jul 13, 2026
Merged

Audit fixes: sorted fingerprints, lzma removal, sdist-true CI - 0.5.0.0#12
aoinoikaz merged 12 commits into
mainfrom
audit-2026-07

Conversation

@aoinoikaz

@aoinoikaz aoinoikaz commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

nova-cache's batch from the July 2026 codebase audit, prepared as the 0.5.0.0 release.

Signing correctness (the audit's one nova-cache HIGH)

C++ Nix computes and verifies narinfo fingerprints over a sorted, deduplicated store-path set. Signing references in the narinfo's file order produced signatures every real Nix client rejects - while nova-cache's own verify, recomputing from the same unsorted order, passed and masked the divergence. fingerprint now sorts by basename and deduplicates before rendering; tests pin unsorted and duplicated reference lists to the sorted fingerprint.

lzma removal

NovaCache.Compression had no consumer anywhere (library, server, or downstream nova-nix), while its default-on manual flag made every Hackage install require system liblzma dev files - cabal install failed on plain Windows and minimal Linux machines while CI stayed green, because CI builds inside the repo where a dev-only constraint pins the flag off. The module, flag, dependency, and its test suite are removed; xz support returns with its first real consumer as a size-bounded decoder suitable for untrusted cache data. Removing an exposed module is a PVP major bump, hence 0.5.0.0.

Server hardening

The whole HTTP surface, previously zero-test executable code, moves into the library as NovaCache.Server (a WAI Application) with a WAI test group covering every route, auth path, limit, and traversal case. Deployment branding stays out of the library - embedders supply their own root page; the bundled executable keeps the cache.novavero.ai landing page in its own module.

  • GET /narinfo-hashes requires the write key and is Cache-Control: no-store - it enumerates the whole store (the public protocol deliberately never offers that) and lists a directory per hit. The push client gains the header in the queued nova-nix follow-up PR.
  • NAR bodies no longer transit memory: uploads stream to a temp file under a running size cap and rename into place atomically; downloads are served from disk via responseFile. A multi-GB NAR previously occupied that much RAM per request in both directions.
  • HEAD is answered wherever GET is (narinfo existence probes previously got 404).
  • Startup fails closed on a CACHE_API_KEY that normalizes to empty (an empty armed key would authenticate an empty bearer token) and on a configured signing key that fails to load (previously a warning, then running unsigned).
  • Configurable bind host (--host / HOST); the default stays all interfaces so existing deployments do not silently rebind.

Packaging and CI

  • The sdist ships NOTICE (Apache-2.0 section 4(d) attribution; the file existed in the repo but not in the released tarball).
  • New CI job builds the unpacked sdist outside the repo - the artifact users actually install - so tree-vs-tarball divergence fails the pipeline instead of surfacing at install time.
  • CI now compiles the server executable and the test suites under -Werror on every OS (previously the server, which holds all handler logic, was never built by CI at all).
  • The deploy job pins cloudflared by version and sha256 instead of pulling latest.
  • The server executable drops its unused crypton dependency; workflows run with a read-only GITHUB_TOKEN.

Verification

cabal build all -f server --enable-tests --ghc-options=-Werror clean, all 9 test groups pass (including the new Server WAI group), ormolu and hlint clean, cabal check clean, and the 0.5.0.0 sdist builds with NOTICE, NovaCache.Server, and the exe's LandingPage present and no compression remnants.

After this merges and releases, nova-nix follows up by dropping its nova-cache -compression constraint and adding its own sdist CI job (that constraint names a flag that no longer exists, so the two must move together).

C++ Nix computes and verifies the narinfo fingerprint over a
StorePathSet - references sorted by basename, deduplicated - so a
narinfo signed in its file order carries a signature every real Nix
client rejects, while nova-cache's own verify (recomputing from the
same unsorted list) passes and masks the divergence.  fingerprint now
sorts and deduplicates before rendering, and the new tests pin an
unsorted and a duplicated reference list to the sorted fingerprint.

Workflows run with a read-only GITHUB_TOKEN: no job writes through it
(checkout, hlint/ormolu annotations, cache, and the SSH/API-key deploy
path are all read-only against the API).
….5.0.0

NovaCache.Compression had no consumer - nothing in the library, the
server, or downstream nova-nix imports it - while its default-on manual
flag made every Hackage install of this package (and every downstream)
require system liblzma dev files, which plain Windows and minimal Linux
machines lack.  cabal install failed for users while CI stayed green,
because CI builds inside the repo where the dev-only constraint pins the
flag off.  A capability lands with its consumer: xz support returns as a
size-bounded decoder suitable for untrusted cache data when substitution
from xz-serving caches arrives.

Also in the release: the sdist ships NOTICE (Apache-2.0 section 4(d)
attribution - the file existed in the repo but not in the tarball), and
the server executable drops its unused crypton dependency (its only
ByteArray use, constEq, comes from ram).

Removing an exposed module is a PVP major bump: 0.5.0.0.
…e sdist

The build matrix compiled only the library - the server executable
(where all handler logic lives) was never built by CI on any OS, and
test suites compiled without -Werror.  Every job now runs
cabal build all -f server --enable-tests --ghc-options=-Werror, and the
compression-flag matrix plumbing (liblzma install, per-OS flag
conditionals) goes away with the flag.

A new sdist job unpacks the cabal sdist outside the repo and builds it -
the tarball users install, not the git tree CI otherwise checks - so
files missing from the sdist or dev-only project settings fail the
pipeline instead of surfacing at install time.
…ptional

Validation now enforces the wire format's per-field spellings: the
narinfo StorePath is absolute and References are bare basenames.  The
other spellings validated - and were then signed - as narinfos every
real Nix client rejects at parse time, and a bare StorePath also derived
an empty store dir inside the signed fingerprint.  parseStorePathBaseName
and parseAbsoluteStorePath expose the per-field parsers.

Store-path names are ASCII-only (Unicode isAlphaNum admitted names Nix
rejects) and capped at 211 characters, both as upstream.  NAR parsing
rejects backslash in entry names - a directory separator on Windows,
this library's primary consumer, so "..\out.exe" is as much a traversal
vector as one with '/' - and rejects nonzero string padding, which
upstream's reader refuses.  Key parsing rejects an empty name or empty
key material at load time: an empty-named key signs every narinfo with
:sig lines no named trust anchor can ever match, surfacing as silent
signature rejection far from the misconfiguration.

Narinfo parsing now mirrors upstream's optionality: only StorePath, URL,
NarHash, and NarSize are required; Compression defaults to bzip2, and
FileHash/FileSize are Maybe (a breaking record change, covered by the
0.5.0.0 major bump).  Valid narinfos from foreign caches no longer fail
to parse over absent optional fields.

Tests pin the lot: the empty-string SHA-256 nix-base32 known-answer
vector (a wrong alphabet or bit order is invisible to roundtrip-only
tests), non-canonical base32 padding rejection, cross-key and
renamed-trust-anchor signature rejection, malformed signature lines,
unsafe/duplicate/out-of-order NAR entry names via hand-encoded wire
bytes, trailing-byte and nonzero-padding rejection, NAR roundtrip edges,
CRLF narinfo parsing, CA field roundtrip, and the four-field minimal
narinfo.  FileStore tests use fresh unique temp dirs instead of a fixed
machine-global path that poisoned later runs and raced checkouts.
@aoinoikaz

Copy link
Copy Markdown
Contributor Author

Extended with the library strictness batch per review discussion: wire-format spellings enforced per field (absolute StorePath, basename references), ASCII-only names with the 211-char cap, NAR backslash-name and nonzero-padding rejection, empty key name/material rejection, upstream-optional narinfo fields (Compression defaults bzip2; FileHash/FileSize now Maybe - breaking, covered by the major bump), plus the known-answer and negative-case test batch (nix-base32 empty-string vector, cross-key/renamed-anchor signature rejection, hand-encoded hostile NAR wire bytes, CRLF/CA narinfo coverage, unique FileStore temp dirs). Full gate green: build all -f server --enable-tests -Werror, all 8 test groups, ormolu, hlint, cabal check, sdist.

createDirectoryIfMissing lost its last caller when the FileStore test
dir moved to unique probed names; -Werror rightly makes the redundant
import fatal.
Routing, write auth, body limits, and the narinfo validation/signing
pipeline move into the library; the executable keeps configuration, key
loading, and its own landing page (embedders supply theirs).  Hardening
riding along: /narinfo-hashes is authenticated and no-store, HEAD is
served wherever GET is, NAR transfer streams via the new FileStore
primitives, an empty-after-normalize CACHE_API_KEY refuses startup, and
the bind host is configurable (--host / HOST, default unchanged).
@aoinoikaz
aoinoikaz merged commit 17586d5 into main Jul 13, 2026
9 checks passed
@aoinoikaz
aoinoikaz deleted the audit-2026-07 branch July 13, 2026 03:30
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