Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

.nornir/signing-design.md

Provenance signing — design

Class: design · siblings: signing-guide.md · plugins-design.md · iceberg-design.md. Strict: this file states only the shape we run — the merkle fold, the signers, the manifest sections. No alternatives, no why.

f0 — detached authenticity over blake3 integrity (the whole)

Every znippy chunk already carries a 32-byte blake3 checksum in the base index — that is integrity. Signing adds authenticity on top: an optional, detached CMS signature, computed as a merkle fold of the per-chunk hashes already in hand — no content is re-read.

law statement enforced by
S-1 signing folds the existing per-chunk blake3 hashes; no content re-read file_digest* fold ordered chunk hashes (sign.rs:108,:122)
S-2 signing is off by default; unsigned archives are byte-unchanged feature sign; blob/compress path untouched
S-3 any change to a chunk hash or artifact changes the archive root archive_root fold (sign.rs:141)
S-4 the merkle root is stable across seal seal_signed_archive_round_trip

Reuse map: signing depends on the CMS/PKCS#8/X.509 stack and the blake3 hashes the writer already computed. It is used by the writer (via with_signer) and by verify. It touches no blob bytes — the compression path is entirely independent.

f1 — the merkle fold

znippy-common/src/sign.rs. Three folds, bottom-up, over hashes already computed:

fn at folds
file_digest(file) sign.rs:108 a file's ordered chunk blake3 hashes → one file digest
file_digest_from_parts(relative_path, ordered_chunks, count) sign.rs:122 same, from explicit parts (path + ordered chunks + count)
archive_root(file_digests, footer) sign.rs:141 all file digests + the footer → the single archive root

The signature is over the archive root, so tampering with any chunk hash or artifact changes the root and breaks verification (tamper_chunk_hash_breaks_verification, archive_root_changes_when_an_artifact_changes, tampering_a_signed_archive_is_detected).

f2 — the signers

znippy-common/src/sign.rs:

item at is
trait ArchiveSigner sign.rs:167 the signing seam — signs the archive root
Ed25519Signer sign.rs:174 ed25519 signer
EcdsaP256Signer sign.rs:202 p256 signer (default algorithm)
signer_from_pkcs8(…) sign.rs:231 builds a signer from a PKCS#8 DER key + X.509 cert

Algorithm is p256 (default) or ed25519.

f3 — where the signature lives

The detached CMS signature is written as reserved manifest sections through znippy-common/src/meta_sink.rs:107 ArrowIpcSink::with_signer(signer). Because the sections are reserved and detached, an unsigned archive is byte-identical whether or not the sign feature is compiled in — the signer only adds sections, never rewrites blobs.

Signed archives round-trip through seal: the merkle root is content-derived and stable, so a sealed signed archive still verifies (seal_signed_archive_round_trip, holger_verifies_a_served_artifact).