# SITE-02 Cryptography and Repository Security Review Packet **Packet date:** 2026-09-14 **Security decision:** `NOT APPROVED` **Production activation:** `BLOCKED` **Independent reviewer:** `UNASSIGNED` **Implementation status:** tested candidate; Linux test/runtime/native-vault evidence, host cross-compiles, and negative exact-commit macOS CI evidence only **Scope:** `crates/apps/nigig-site` encrypted repository, native key binding, writer shutdown, migration design, and release gates This packet is deliberately not a self-approval. Passing unit tests, Clippy, or a Linux desktop smoke test cannot substitute for independent cryptographic review, native-vault evidence, crash testing, recovery policy, or release governance. First-run setup and all migration execution remain hard-locked with support code `SITE-02-SECURITY-REVIEW-REQUIRED`. ## 1. Decision The candidate materially improves the SITE-01 containment baseline, but it is **not fit for production activation**. It provides a reviewable `NIGIG2` envelope, exact existing-key lookup, encrypted atomic-publication attempts, canonical readback, rollback attempts, a bounded process lock with stale-writer rejection, a one-slot coalescing writer, explicit accepted/durable health, and test-only migration exercises. It does **not** yet provide an approved setup, recovery, escrow, rotation, deletion, trusted anti-rollback, per-site storage, or production migration lifecycle. The detailed lifecycle proposal is in `SITE_02_KEY_LIFECYCLE_DESIGN.md`. It selects an offline asymmetric recovery candidate for review, defines setup/rotation/recovery/deletion state machines, and proposes a native-vault rollback anchor. It is explicitly unapproved and has no production implementation. No reviewer has approved: - AES-GCM primitive use on every supported processor; - the 96-bit random-nonce policy across process restarts/devices; - associated-data and envelope identity semantics; - OS credential-store behavior on Linux, macOS, and Windows; - key creation, rotation, recovery, escrow, and secure deletion; - filesystem crash/power-loss behavior; - legacy-original retention/disposition; or - activation of setup or migration UI. ## 2. Candidate boundary ### Compiled production behavior - Normal open recognizes only strict `NIGIG2` authenticated envelopes and enables writes only for the exact current store schema. After authentication, it probes the scalar schema version as a `u64` before decoding the current shape: syntactically valid older/future shapes enter distinct preserved recovery/migration states, while an exact-current document is decoded with unknown root fields denied rather than silently discarded. - The envelope is parsed before lookup of the exact `(store_id, key_id)` native credential. - A missing, invalid, retired/rotated, unavailable, or wrong key is not replaced. - Absence enters setup-required safe mode; it creates no directory, file, key, or demo data. - Raw JSON, `NIGIG1` plaintext envelopes, and encrypted `NIGIG1` envelopes are classified for recovery/migration and are not opened by the normal runtime. - First-run setup and migration entry points return `SITE-02-SECURITY-REVIEW-REQUIRED`. - Confidential mutations require a valid explicit selected-site scope, except the profile-level site create/select operations; both mutation APIs are crate-private, serialize zeroizing before/after fences that discard out-of-scope changes, and no read helper silently selects the first site. - Accepted revisions are visibly distinct from durable revisions. - Normal application destruction requests a bounded five-second writer drain and logs only a content-free support code on failure. ### Deliberately non-production behavior `migrate_legacy_json` and its plaintext/legacy decryptors compile only under `cfg(test)`. They exercise the proposed consent and preservation contract but cannot be invoked by a production binary. The broad media/export and live-network fixtures are additionally limited to Linux test builds, keeping them out of macOS/Windows native-repository test graphs. There is no production key-creation API in the candidate. ## 3. Envelope specification under review The binary header is fixed at 68 bytes: | Offset | Bytes | Meaning | Validation / binding | |---:|---:|---|---| | 0 | 6 | ASCII `NIGIG2` | Exact match | | 6 | 1 | format version (`1`) | Unsupported versions rejected | | 7 | 1 | algorithm (`1` = AES-256-GCM) | Unsupported algorithms rejected | | 8 | 16 | random `store_id` | Non-zero; native-key routing; AEAD AAD | | 24 | 16 | random `key_id` | Non-zero; native-key routing; AEAD AAD | | 40 | 8 | big-endian repository revision | Non-zero; AEAD AAD | | 48 | 12 | random GCM nonce | AEAD AAD and nonce input | | 60 | 8 | big-endian plaintext length | Checked before allocation; AEAD AAD | | 68 | variable | ciphertext plus 16-byte GCM tag | Exact total length; authenticated | The full fixed header is passed as AES-GCM associated data. Header identity, revision, nonce, declared length, and algorithm/version are therefore authenticated once the exact key is available. A routing-identity change can fail as `KeyMissing` before AEAD because the altered identity deliberately selects a different native key record; changes that retain key routing fail authentication. ### Primitive - AES-256-GCM via locked `aes-gcm` 0.10.x and `aead` 0.5.x. - 256-bit DEK, 96-bit nonce, 128-bit tag. - Plaintext and key buffers use `zeroize::Zeroizing` where owned by this crate. The locked `aes-gcm`, `aes`, `ghash`, and `polyval` dependency features enable their available temporary-key, key-schedule, and hash-state zeroization paths; this reduces residual state but is not a proof against compiler-created copies, process dumps, or abrupt termination. It is also incomplete upstream: `polyval` 0.6.2's autodetect union uses `ManuallyDrop`, and its ARMv8 PMULL backend explicitly leaves zeroization unimplemented. No complete cryptographic-state-erasure claim is made. - No plaintext encryption fallback exists. - A fixed envelope known-answer vector independently generated through Node.js/OpenSSL verifies the exact header, AAD, ciphertext, and tag bytes; every truncated prefix and an extended form are rejected. - Maximum current whole-store plaintext: 64 MiB; declared and observed sizes are checked before decryption/large allocation, including a sparse oversized-file repository test. The upstream `aes-gcm` documentation reports a 2020 NCC Group audit with no significant findings, but also warns that its portable implementation is not suitable on processors with variable-time multiplication. That hardware precondition is unresolved (B7). ### Nonce policy Each seal obtains a fresh 96-bit value from the operating-system RNG. A process-local atomic guard refuses more than `2^32` random-nonce invocations. This follows the broad SP 800-38D random-IV invocation ceiling but does **not** durably account per key across restarts, concurrent processes, restored device images, or multiple devices. Production approval requires a reviewed durable policy and rotation/reconciliation rules (B5). References for reviewers: - NIST SP 800-38D: - `aes-gcm` 0.10.3 documentation: - `keyring` ecosystem guidance: ## 4. Native key binding under review The production crate links `keyring-core` plus one explicit provider per desktop target: | Target | Provider candidate | Evidence status | |---|---|---| | Linux | Secret Service through `zbus-secret-service-keyring-store` | Disposable real vault invalid/active/retired/missing/locked states plus two encrypted revisions/reopens, Unix mode/link checks, and wrong/missing-key preservation pass; no user vault touched | | macOS | Keychain through `apple-native-keyring-store` | The `aarch64-apple-darwin` library/test graph cross-check compiles and lints. Exact-commit Gitdab attempt 2 also completed checkout plus target-native check/Clippy, but its former 65-minute job bound expired during clean test-profile code generation before any contract or disposable Keychain test executed | | Windows | Credential Manager through `windows-native-keyring-store` | The `x86_64-pc-windows-msvc` library/test graph, including fail-closed `Local` persistence and reparse/link-count contracts, compiles and lints; explicit isolated native-vault/filesystem execution is declared but not run locally | Credential account names derive only from hexadecimal `store_id:key_id`; no PII is included. Native records are versioned as one state byte plus a 32-byte DEK: `0x01` active and `0x02` retired. Active all-zero key material is rejected as invalid. A retired marker is differentiated from a deleted/missing record. Provider initialization now rejects a credential store that does not advertise `UntilDelete` persistence. The candidate can read this representation but intentionally provides no production create, rotate, retire, recover, export, or delete operation. The all-in-one `keyring` facade is not a direct production dependency. `CredentialPersistence::UntilDelete` proves only lifetime class, not non-roaming or non-rollback behavior. The locked Windows adapter documents `Enterprise` as its default for newly written generic credentials and warns that operations on one entry from different threads are not reliably ordered; Microsoft documents that enterprise persistence may expose a credential to the same user on other computers. The candidate now rejects a Windows record whose reported persistence is not exactly `Local` before retrieving its secret. Production creation is absent today, so a reviewed setup/rotation design must still explicitly create and target-natively verify local records and serialize same-entry updates. The locked Apple adapter and feature selection use its legacy User/login keychain by default; Apple documents that a macOS keychain file can be restored from Time Machine. Apple also documents that an iOS/iPadOS local device keychain participates in same-device iCloud backup/restore. Exact deployed backup/restore behavior still requires target-native verification and therefore cannot close B8. These semantics prevent treating a native record as an inherently trusted monotonic anchor. Provider initialization and error mappings still require platform-owner review and real OS-vault tests. Platform-semantics references for reviewers: - Microsoft `CREDENTIAL` persistence values: - Apple macOS Keychain and Time Machine restore: - Apple iCloud Backup and local device-keychain restore: - GNOME libsecret locking/error model: ## 5. Publication and durability design under review For a newer revision, the repository currently attempts: 1. reject symlinks in existing managed path components and managed artifacts; on Windows reject every reparse-point component; 2. open a non-following, owner-checked `0600` lock file and acquire an exclusive process lock within five seconds; Windows opens an existing lock with `FILE_FLAG_OPEN_REPARSE_POINT`, rejects reparse metadata, and requires one link through handle metadata; 3. reopen the canonical document without following a final symlink/reparse point; on Unix require a private file mode, one link, the parent owner, and a parent that is not group/other writable; on Windows require a one-link handle; then verify identity/revision still exactly match the caller's metadata; 4. look up the exact existing DEK; 5. authenticate the current canonical ciphertext under that DEK before creating any publication artifact, so a matching but tampered header cannot authorize overwrite; 6. serialize into a zeroizing in-memory buffer; 7. encrypt before opening a temporary file; 8. create a unique temporary file with `create_new` and, on Unix, no-follow/close-on-exec flags and mode `0600`; 9. write all ciphertext, explicitly `flush`, and `sync_all` the file; 10. rename the old canonical ciphertext to a unique rollback name; 11. rename the new ciphertext into the canonical name; 12. synchronize the parent directory where supported; 13. reopen without following a final symlink, parse, decrypt, and byte-compare canonical ciphertext; 14. remove the rollback ciphertext; and 15. synchronize the parent directory again. The only persistent companion is an empty `..lock` coordination file; it contains no identity, revision, key material, or domain data. Failures before publication clean the temporary file and preserve the canonical bytes. Injected failures after publication attempt to restore the prior canonical ciphertext. A subprocess harness now terminates without unwinding at all 12 exercised commit fault stages through readback, including the explicit flush stage, and verifies that original ciphertext remains recoverable, publication artifacts force recovery, and no sentinel plaintext is present. Unexpected temp/backup artifacts are checked both before and after lock acquisition and force recovery instead of automatic cleanup, including when a prior process dies while a writer waits. The process lock plus in-lock authenticated canonical revision check prevents a matching-but-tampered current envelope or a second cooperative instance from silently authorizing overwrite; a simultaneous two-writer test proves exactly one revision-2 commit wins and the stale peer fails. This is safer than silently choosing a revision, but it is not proof against hostile/uncooperative writers or all filesystem, kernel, device-cache, antivirus, cross-platform, or power-loss behaviors (B6, B8, B9, B11). `File::sync_all` only attempts to synchronize content and metadata; actual persistence guarantees remain filesystem/platform dependent: . ## 6. Writer and UI health semantics - One process-wide pending slot coalesces complete snapshots; queue depth is at most one. - Every accepted snapshot includes all prior accepted in-memory changes. - The writer serializes publication and may skip intermediate revision numbers when pending snapshots coalesce. - Health exposes accepted revision, durable revision, pending depth, active state, accepting state, and last typed failure. - `accepted > durable` is explicitly labelled `UNSAVED`. - A writer failure is sticky and blocks later closures before they mutate the canonical in-memory value. - Graceful shutdown stops acceptance, drains the pending slot within a caller-supplied bound, and joins only after the worker reports completion; dropping a writer without that explicit path now at least wakes its idle worker and requests a non-blocking drain/stop. This is still one whole-store writer rather than the plan's final per-site repository/writer architecture. The scope fence excludes the legacy supplier directory because its records have no `site_id`; that shared/ambiguous model must be assigned or quarantined in SITE-03. Cooperative process exclusion and stale-revision rejection now exist, but contention/crash campaigns, per-site isolation, external change ingestion, and hostile-writer handling remain unresolved (B11). ## 7. Migration contract exercised in tests The test-only migration controller enforces: - explicit consent before source/key access; - a distinct, absent target path; - an already-provisioned exact target key (no create-on-lookup-failure path); - one bounded in-memory source snapshot; - separate handling for raw/explicit plaintext and encrypted `NIGIG1`; - failure without a target when the historical key is missing; - authenticated `NIGIG2` target publication and semantic readback; and - byte-identical preservation of the legacy original. Production migration remains absent. Retention duration, backup interaction, legal/user-confirmed deletion, secure-erasure claims, interrupted GUI resume behavior, and rollback tooling are unresolved (B3, B4, B10). ## 8. Current evidence Evidence produced locally on Linux with Rust/Cargo 1.97.1: | Gate | Current result | |---|---| | `cargo test --locked -p nigig-site` | Library 81 passed and 1 live-vault test explicitly ignored; binary 1 passed; integration 6 passed and 1 live-server test explicitly ignored; doc tests 0 | | `cargo test --locked -p nigig-site --lib` | 81 passed, 0 failed, 1 live-vault test explicitly ignored | | `crypto::tests` | 13 passed, including the independent fixed vector and all-zero pre-use rejection | | `repository::tests` | 29 normal tests passed; dedicated ignored live-vault test also passed separately | | `store::tests` | 9 passed, including version-first arbitrary-shape classification, strict current root-schema rejection, and cross-site/profile mutation-fence rejection | | symlinked Unix `TMPDIR` regression | Repository 29 passed/1 live-vault ignored, store 9 passed, and crypto 13 passed below a logical symlink whose physical target was resolved before fixture creation; explicit managed-parent/artifact symlink rejection remained green | | `cargo check --locked -p nigig-site --all-targets` | Passed | | crate-owned Clippy, all targets, `--no-deps -D warnings` | Passed | | crypto feature resolution | `aes-gcm`, AES schedule, GHASH, and POLYVAL `zeroize` features present for Linux x64, Windows x64 MSVC, and macOS ARM64 target graphs | | workflow source/ownership assertions | Passed locally, including path coverage, immutable action/dependency revisions, trusted-event native-host restriction, pre-build artifact and disposable-Keychain canaries, hard locks, and no Cargo failure suppression | | macOS disposable-Keychain wrapper | Bash syntax and mocked Darwin success, Cargo-failure, cleanup, default restoration, and untrusted-event refusal passed; real `security`/Keychain execution remains mandatory target evidence | | `Cargo.lock` SHA-256 | `ad166a13f0b3e51f9b9b0cde743adbbd3413db2ee0d441faadfb24033042795b` (dependency versions unchanged; crypto-backend `zeroize` and existing `windows-sys` package edges enabled) | | RustSec production graph | Conservative all-feature/all-target traversal: 0 findings across 356 reachable normal/build packages; all 6 workspace findings proved dev/unrelated-workspace only; DB commit `b50980aad8b8f14f77e25a97b32dd94bf008b0af` | | desktop binary build | Passed; release ELF SHA-256 `fea50fa70e75a331b3d3e6625cac945332ad16de6a1a4f39fd7afcc487d38762` | | production binary/source sentinel scan | No fixture plaintext/native-vault test markers in the release binary; no production native-key write/create or plaintext-publication token | | X11 real-binary smoke | Rendered safe mode, closed through WM close, exited normally, created no repository data | | Linux native vault | Disposable Gnome Keyring invalid/active/retired/missing/locked states, two encrypted revisions/reopens, private/single-link canonical checks, and wrong/missing-key preservation passed; disposable root removed | | Windows/macOS provider compile | Host-side library-and-test-target check plus crate-owned Clippy passed for Windows x64 MSVC and macOS ARM64 targets; the Windows GNU library test executable also cross-linked as PE32+ | | Exact-commit Gitdab macOS CI | Published commit `41259b096ac395acbb6bc1bd29b0922cd7faa56d`, workflow run `#1293` / API object `1398`: exact checkout, target-native check, and crate-owned Clippy passed; all 13 crypto contracts passed after a 51-minute clean test-profile build. Repository execution then produced 5 passes, 23 failures, and 1 ignored test because the macOS logical temp root `/var/folders/...` traverses the `/var` system symlink and every security fixture was rejected before its intended assertion. Store contracts and the disposable Keychain lifecycle never ran. The patched v4 artifact action found both logs but its Twirp `CreateArtifact` request timed out five times and remained stuck until the 120-minute job deadline; no artifact was created. This is negative defect evidence, not target-native security evidence | | Windows/macOS execution | Still absent. The remediation candidate resolves only the test-owned Unix temp root to a physical path, keeps production and explicit attack-path symlink rejection strict, stages artifact-protocol and disposable-Keychain canaries before compilation, uses the immutable v3-node20 legacy uploader after the observed v4 failure, and wraps Apple execution in a temporary user-default CI keychain with mandatory restoration/deletion. These changes are locally and cross-target validated but have not yet passed a real macOS or Windows runner | | independent review | Not performed | The 120-minute budget exposed real defects rather than converting incomplete work into a pass. The remediation does not raise that budget again: it fixes the test-only logical-path mismatch, fails fast on artifact transport and disposable-Keychain readiness before the expensive build, moves this Gitdab deployment back to the pinned legacy artifact protocol, and isolates the Apple test credential in a disposable CI keychain. None of that counts as macOS evidence until a replacement exact-commit run finishes every Rust contract, the real Keychain lifecycle, cleanup, and downloadable evidence upload within the existing bound. The broad media/export and live-network fixtures are now Linux-CI-only, so the exact Windows MSVC and macOS ARM64 library test graphs can be type-checked and linted from the Linux host. Those commands still do not link or execute native credential/filesystem behavior, so the workflow continues to require actual Windows and macOS runners; cross-checking is not counted as target-native evidence. These are candidate-development results, not release evidence. CI results must be attached to the exact reviewed commit, and the Gitdab branch/status policy must prevent bypass. ## 9. Required blocker disposition | ID | Blocker | Required evidence to close | Status | |---|---|---|---| | B1 | No independent cryptography/security reviewer | Named qualified reviewer, dated review, exact commit hash, signed decision and findings disposition | OPEN | | B2 | No approved first-run key setup | Reviewed UX/consent flow; atomic repository/key transaction; orphan cleanup; real vault evidence; no automatic setup | OPEN | | B3 | No approved user-controlled recovery/escrow design | Threat model, key wrapping/KDF choice from established construction, recovery authentication, loss/abuse analysis, restore drill | OPEN | | B4 | Rotation, retirement, revocation, and deletion are incomplete | State machine, old-key availability policy, crash recovery, backups, audit events, secure-deletion claims bounded by platform reality | OPEN | | B5 | Nonce accounting is only process-local | Reviewed per-key durable/multi-process/device invocation strategy, collision analysis, ceilings, forced rotation and exhaustion tests | OPEN | | B6 | No trusted anti-rollback anchor; a native record may be restored with ciphertext | Authenticated genuinely non-rollback monotonic authority or explicitly bounded reconciliation design; coordinated backup/device rollback analysis; downgrade and rollback tests | OPEN | | B7 | AES-GCM hardware timing assumptions unresolved | Supported CPU/platform matrix proving constant-time multiplication requirements or approved alternative primitive/implementation | OPEN | | B8 | Native providers/filesystem semantics remain unverified on macOS and Windows | Target-native locked/unlocked/missing/corrupt vault tests, permission/reparse tests, rename/replace, directory durability, and shutdown tests on both OSes | OPEN | | B9 | Abrupt-exit harness exists, but no real power-loss/filesystem campaign | Native SIGKILL/termination plus remount/reboot or equivalent tests on each filesystem/OS, artifact-state matrix, recovery operator procedure | OPEN | | B10 | Migration retention/disposition and GUI E2E absent | Existing raw and encrypted `NIGIG1` GUI tests; consent audit; restart/resume; backup treatment; explicit retain/delete policy | OPEN | | B11 | Whole-store writer still lacks final per-site/external-change reconciliation | Per-site bounded writer/repository design; lock contention/crash and shutdown tests on every OS; external revision refresh/conflict UX; hostile-writer analysis | OPEN | | B12 | Release governance is not independently enforced | Protected Gitdab branch, required exact-commit statuses, independent approval identity, non-bypassable activation and published artifact provenance | OPEN | ## 10. Reviewer checklist A reviewer must independently verify, not merely accept author assertions: - [ ] Exact envelope parser bounds and integer arithmetic. - [ ] Full-header AAD coverage and identity/key lookup behavior. - [ ] AES-GCM implementation and supported-hardware assumptions. - [ ] RNG failure behavior and nonce uniqueness/invocation analysis. - [ ] Key-record format, provider selection, locked-vault behavior, concurrency, and lifecycle. - [ ] No plaintext in canonical, temp, backup, journal, crash, support, or CI artifacts. - [ ] Publication/rollback behavior under injected and real crash/power-loss faults. - [ ] Writer coalescing, sticky failure, bounded shutdown, and unsaved-state UX. - [ ] Explicit consent, old-key lookup-only behavior, preservation, verification, and disposition in migration. - [ ] Recovery/escrow design and abuse/loss scenarios. - [ ] Exact-commit CI, branch protection, approval identity, and activation controls. ## 11. Sign-off (intentionally blank) | Role | Name | Organization | Exact commit | Decision | Date | Signature/reference | |---|---|---|---|---|---|---| | Independent cryptography reviewer | UNASSIGNED | — | — | NOT REVIEWED | — | — | | Independent application-security reviewer | UNASSIGNED | — | — | NOT REVIEWED | — | — | | Platform owner (Linux) | UNASSIGNED | — | — | NOT REVIEWED | — | — | | Platform owner (macOS) | UNASSIGNED | — | — | NOT REVIEWED | — | — | | Platform owner (Windows) | UNASSIGNED | — | — | NOT REVIEWED | — | — | Until every required blocker is closed and an exact-commit approval is published, setup/migration must remain locked, SITE-02 must not be called complete, and SITE-03 must not begin.