How the system specified in secsec-Design.md is built: the crate layout, the key
dependencies and why they were chosen, the test and assurance strategy, the security-critical risk
register, and what each component does. It does not restate the design: secsec-Design.md says
what, this says how.
Posture. secsec is a security-critical cryptosystem whose entire value is "the server cannot read your data." A subtle bug is silent and total, so correctness and provability dominate speed. Two rules: (1) no security-critical module is trusted until its tests prove it (KATs, property tests, and negative tests, committed with the code); (2) the implementation is built to be audit-ready and should get an independent professional cryptographic review before it touches irreplaceable data. This document does not replace that review.
What the tests cannot establish. The CMT-4 guarantee is a theorem of the CTX paper; the tests
show the construction matches it byte for byte and rejects cross-key opens, not the reduction.
Constant-time behavior is by construction (subtle compares), not measured on hardware. The
primitive crates (blake3, ssh-key, libcrux-ml-kem, x25519-dalek, chacha20, poly1305) are
trusted as vetted; the tests check secsec's use of them (labels, input layouts, combiner order)
against external references where those exist.
Scope. Transport is QUIC/TLS-only (the pinned self-signed host key is the sole trust anchor;
no CA, no stdio/SSH mode). Device keys are Ed25519-only. The keyslot KEM is X-Wing
(ML-KEM-768 ⊕ X25519): post-quantum, the one harvestable asymmetric exposure. Targets are Linux,
macOS, and Windows (x86_64 and aarch64). secsec is single-host: one repo on one blind server
(secsec sync takes one --server).
One repository, a Cargo workspace, producing a single binary (secsec) whose subcommands are the
client and whose serve subcommand is the server. It is split into focused libraries so the
security-critical cores are small, independently testable, and separately reviewable.
secsec/
├── crates/
│ ├── secsec-canon/ §9.3 canonical encoding: fixed-width LE, bounded lengths, re-encode guard
│ ├── secsec-aead/ §9.4, §9.8 CTX/CMT-4 committing AEAD, plus the fresh-nonce AEAD for mutable blobs
│ ├── secsec-kdf/ §5, §9.5 master-key generations, every derive_key family, mk_commit, the key ring
│ ├── secsec-chunk/ §9.7 keyed FastCDC-style chunker (gear table from cdc_seed)
│ ├── secsec-sig/ §5, §9.6 device key loading, Ed25519-only SSHSIG, seal and X-Wing seeds
│ ├── secsec-frame/ §9.1, §19 FRAME, object types, the §19 decoder bounds
│ ├── secsec-object/ §9.2, §9.7 content addressing, seal/open with three-way verify, chunk padding
│ ├── secsec-store/ §13, §15 redb blob store: server repository and client object cache
│ ├── secsec-pq/ §8.3, §17 X-Wing keyslot wrap, draft-10 conformant
│ ├── secsec-roster/ §8 sigchain fold/succession, per-entry AEAD, key histories, revoke closure
│ ├── secsec-sync/ §8.5, §10 signed+encrypted Head, DAG ancestry, three-way merge, rollback gates, frontier
│ ├── secsec-proto/ §12, §15 wire codecs, per-op args_hash and auth, prune hashes, server limits
│ ├── secsec-snapshot/ §6, §10 Tree/Commit graph, directory snapshot and reconciling restore
│ ├── secsec-transport/ §11 pinned/TOFU QUIC configs, application handshake, stream framing, client RPC
│ ├── secsec-engine/ §10 stored-tree ↔ merge-node bridge, sibling acceptance, signed merge commits
│ ├── secsec-server/ §11, §12 per-op pipeline, authorized_keys gate, serve loop
│ ├── secsec-client/ §7, §8, §10, §15 Remote trait, repo lifecycle, pairing, sync, history, retention, watcher
│ └── secsec-fuzz/ §18 one harness body per decoder of untrusted bytes
├── bin/secsec/ the CLI and the server entry point
├── fuzz/ cargo-fuzz targets (nightly), one per secsec-fuzz body; outside the workspace
├── vectors/ committed known-answer vectors
├── xtask/ the KAT anti-drift check and the release recipe
├── ui/ GNOME Shell extension, macOS menu-bar app, their installer
├── install.sh release installer: client, --server, --binary
├── assets/ brand SVGs
└── .github/workflows/ ci.yml, release.yml
Dependency layering (strictly downward; no crate depends on a higher layer):
| Layer | Crates | Internal dependencies |
|---|---|---|
| 0 | canon, aead, kdf, chunk, sig |
none |
| 1 | frame |
canon |
| 2 | object, store, pq, roster, sync, proto |
object: kdf, frame, aead · store: frame · pq: aead, canon · roster: sig, frame, canon, aead, kdf · sync: canon, frame, kdf, sig, aead · proto: sig, canon, frame |
| 3 | snapshot, transport |
snapshot: canon, frame, kdf, object, chunk, store, sig · transport: sig, canon, frame, proto |
| 4 | engine, server |
engine: snapshot, sync, store, kdf, object, frame, sig · server: proto, store, sig, frame, transport |
| 5 | client |
store, snapshot, object, frame, kdf, canon, sync, engine, roster, pq, sig, transport, proto |
| 6 | fuzz, bin/secsec, xtask |
the crates they exercise |
secsec-sync keeps the §10 merge, DAG, and rollback logic storage-free and purely testable;
secsec-engine is the only §10 code that touches store and snapshot (it loads stored trees into
the merge model, re-seals the result, and authors the signed merge commit). The Remote trait lives
in secsec-client, with two implementations: QuicRemote over a live connection and a test-only
in-process MemRemote over a real store.
Pinned by Cargo.lock, minimal, no OpenSSL. The security-relevant choices:
libcrux-ml-kem: ML-KEM-768, formally verified (FIPS 203). The X-Wing keyslot (secsec-pq) is built directly on it (single-seedSHAKE256(sk, 96)expansion viasha3, label-last combiner, the FIPS 203 §7.1 pairwise check at every derivation, §7.2 validation of every published key), so no third-party X-Wing crate is trusted.x25519-daleksupplies the X25519 half, whose secret comes from that same expansion, never from the Ed25519 key.chacha20+poly1305: there is no drop-in committing-AEAD crate, sosecsec-aeadbuilds the CTX/CMT-4 construction from the raw primitives (one-time Poly1305 key from ChaCha20 block 0,ctx_tag = BLAKE3::keyed_hash(...),Tnever stored).chacha20poly1305is a dev-dependency only: tests cross-check the keystream and tag against it byte for byte.ssh-key(RustCrypto, featuresstd,ed25519,encryption): SSHSIG with per-namespace domain separation (§9.6), Ed25519-only, passphrase-encrypted private keys decrypted in memory.sha2serves only the legacy v1 local-seal key (the clamped scalar), read for migration.quinn+rustls(theringprovider): QUIC over TLS 1.3 with a pinned suite list and X25519 key exchange; the custom pinnedServerCertVerifier(R1) is the sole transport-auth path.x509-certextracts the certificate SPKI;rcgengenerates the server's self-signed host key.blake3: the KDF and hash backbone (§9.5); the keyed chunker's gear table is composed directly on its XOF rather than a third-party chunking crate.redb(embedded store, one file),notify(filesystem watch),filetime(restored mtimes),zeroize/subtle/getrandomfor key hygiene, constant-time compares, and the OS CSPRNG.secrecy,region, andmlockare NOT WIRED.- The CLI adds
clap,rpassword(no-echo passphrase prompt),socket2(dual-stack UDP bind),tempfile,tokio, andrustix(unix signals forsecsec stop).
Every security-critical crate carries, in the same change as the code:
- Known-answer vectors.
vectors/secsec-kat-v1.txtpins nine sections: all eleven §9.5derive_keyfamilies plusmk_commit, the FRAME, the CTX AEAD, an object, a head blob, the session transcript, v1 and v2 roster entries and a roster-key-history wrap, chunker cut points, and the §7 pairing slots and MACs. Each section names the inline test that asserts it, andcargo xtask vectors --check(also the testcommitted_vectors_match_live_code) recomputes every value from live code and fails on drift. X-Wing is checked against its published vector:xwing_katinsecsec-pqasserts byte-identity with draft-10 Appendix C. - Property tests (
proptest): AEAD round trips, wrong keys, and single-bit flips; canonical round trips and appended-byte rejection; the sigchain fold against an independent reference model (R5).secsec-aead,secsec-canon, andsecsec-frameeach carry atests/robustness.rsthat feeds arbitrary bytes to the decoder and requires no panic. - Mandatory negative tests (the ship-broken spots):
- the TLS verifier: another key fails the pin; a garbage handshake signature fails; TLS 1.2 is refused; a MITM key fails a real TLS and QUIC handshake; TOFU records nothing before the signature verifies.
- SSHSIG: a genuine ECDSA signature and an ECDSA key are rejected; a wrong namespace fails.
- keyslots: an
algo_idabove this build's asks for an upgrade, one below is unsupported; a forged keyslot failsmk_commit; a grant refuses an invalid X-Wing public key before writing anything. - rollback: a roster, commit-version, or head-version rollback is rejected; a DAG missing gate metadata fails closed; a forged ancestor commit is rejected before any gate reads it.
- storage: a lost
cas-headpromotes nothing; a stale prune deletes nothing; a prune racing a push cannot dangle the new head.
- Fuzzing.
secsec-fuzzholds eleven harness bodies (frame, wire, roster_entry, keyhist, keyslot, object, head, frontier, tree, commit, pairing);fuzz/wraps each as acargo-fuzztarget. On stable,every_decoder_survives_arbitrary_inputruns every body over a fixed-seed corpus, andfuzz_manifest_lists_every_targetkeepsfuzz/Cargo.tomlin step with the harness. - Misuse-resistant by construction: the committing AEAD takes a
UniqueKey(a key bound to one sealing) and fixes the nonce; the mutable AEAD takes aFreshNoncefrom the OS CSPRNG. No API pairs a long-lived key with a caller-supplied counter. - Lints:
unsafe_code = "forbid"and clippyall = "deny"for every member, which an xtask test (every_workspace_member_inherits_the_workspace_lints) enforces; release builds keep overflow checks and abort on panic.
CI (ci.yml, six jobs, RUSTFLAGS=-D warnings): lint (cargo fmt --check, cargo clippy --all-targets --all-features -D warnings, cargo xtask vectors --check); test (cargo test --all --all-features on Linux, macOS, and Windows); msrv (cargo check on Rust 1.89); audit
(cargo audit, ignoring only RUSTSEC-2023-0071 in .cargo/audit.toml: the optional rsa
dependency of ssh-key this workspace never enables); scripts (shellcheck of install.sh, ui/install.sh, ui/macos/build.sh, and node --check of the GNOME extension); menubar (builds the macOS app). Every cargo step runs --locked.
Release (release.yml, on rc* and v* tags): runs CI, builds secsec for Linux (static
musl, stripped), macOS, and Windows on x86_64 and aarch64 with the tag stamped in as SECSEC_RELEASE
(what secsec --version prints; other builds print the crate version), packages the GNOME extension and the
macOS app, and publishes them with SHA256SUMS and build-provenance attestations, which install.sh
verifies with gh attestation verify when the GitHub CLI is signed in. cargo xtask release prints a
reproducible build recipe (fixed SOURCE_DATE_EPOCH, remapped paths) that the release workflow does
not follow (NOT WIRED).
| # | Hotspot | Failure mode | Mitigation |
|---|---|---|---|
| R1 | Custom rustls verifier (§11) | return Ok(()) or a stubbed signature check silently disables auth |
SPKI pin compared in constant time; verify_tls13_signature delegated, never stubbed; TLS 1.2 refused; negative and MITM tests gate CI |
| R2 | CTX from raw Poly1305 (§9.4) | wrong T recomputation, a high-level open, a non-constant-time compare |
secsec-aead isolated; reference cross-check and KATs; commit check in constant time before any decryption |
| R3 | X-Wing keyslot (§8.3, §17) | a non-conformant combiner or seed expansion; a keyslot derived from public data | draft-10 KAT (byte-identity); FIPS 203 §7.1 check and §7.2 validation; the X-Wing seed derives from the Ed25519 seed, never the scalar; CTX-committing AEAD over the wrap |
| R4 | Rollback-aware merge (§10) | a replayed old commit or head steers the merge; a lost write | ancestor no-op before the gates; roster, commit-version, and head-version gates; frontier sealed before every ref-advancing push; keep-both merge |
| R5 | Sigchain fold, cold start, roster-key peel (§8) | a mis-fold gives the wrong membership; a bootstrap deadlock | model-based fold test; explicit cold-start order; RFP and mk_commit checks; persisted anti-rollback anchor (P7) |
| R6 | Transactional push and retention (§15) | a promoted head references a missing object, or retention deletes live data | promote and ref swap in one redb transaction (I1); pushes stage everything outside the remote head's tree, durable ids included; chunk-only prune under the all_heads_hash + roster_len CAS; retention_keep_versions = 0 keeps everything |
| R7 | Canonical serialization (§9.3) | malleability leads to a signature bypass | bounded lengths, no trailing bytes, re-encode guard over received bytes; fuzzed |
| R8 | Keyed chunking (§9.7) | unkeyed boundaries allow cross-repo size fingerprinting | gear table from the generation-scoped cdc_seed; power-of-two chunk padding blurs the size signal (§21) |
-
Canonical encoding (
secsec-canon, §9.3). A hand-written writer and strict reader: fixed-width little-endian integers,le32(len) ‖ bytesstrings whose length is checked against a caller-supplied bound before the body is read, raw fixed fields, andfinish()rejecting trailing bytes.verify_reencodeenforces that signed or hashed bytes decode to a value that re-encodes to exactly those bytes. -
Committing AEAD (
secsec-aead, §9.4, §9.8). CTX/CMT-4 over ChaCha20-Poly1305: a unique key per sealing, a fixed zero nonce, andctx_tag = BLAKE3::keyed_hash(key, "secsec-ctx-v1" ‖ AD ‖ T)where the raw Poly1305 tagTis recomputed on open and never stored. Open is three-phase (MAC, constant-time commit check, then decrypt), so no plaintext exists before the commitment verifies. A separate fresh-nonce RFC 8439 variant (seal_mut/open_mut) serves the mutable head blob and the sealed local frontier. -
Key hierarchy (
secsec-kdf, §5, §9.5).MasterKeyholds a generation and its key (zeroized on drop); every subkey isBLAKE3::derive_key(label, IKM)streamed through a wiped hasher, andmk_commitis the onekeyed_hash. TheMasterKeystrait resolves a generation to its key, so a key ring opens objects and heads sealed under any past generation, andref_name_keyalways comes from generation 1. -
Chunking (
secsec-chunk, §9.7). A gear rolling hash with normalized two-mask cut points (16/64/256 KiB), the 256-entry gear table drawn fromBLAKE3::keyed_hash(cdc_seed, "secsec-cdc-gear-v1")in XOF mode and wiped on drop.chunk_streamholds at most one maximum chunk in memory and cuts byte-identically to the in-memory path. -
Object plane (
secsec-frame,secsec-object, §9.1, §9.2). Objects areFRAME ‖ ctx_tag ‖ ciphertext, content-addressed bykeyed_hash(id_key[g][t], FRAME ‖ path_salt ‖ plaintext). On open the generation resolves through the key ring, the FRAME must equal the expected one, the CTX tag verifies under the id-derived key, and the id is re-derived and compared in constant time. Chunks are padded to the next power of two above their length before sealing.secsec-framealso owns the §19 constants every decoder checks before allocating. -
Device identity (
secsec-sig, §5, §9.6). Loads an OpenSSH Ed25519 private key (decrypting a passphrase-protected one in memory), signs SSHSIG under the six disjoint namespaces, and verifies only when both key and signature are Ed25519. It derives the two private-seed secrets: the v2 local seal key and the X-Wing seed (plus the legacy v1 seal key for migration). -
Hybrid-PQ keyslot (
secsec-pq, §8.3, §17). X-Wing = ML-KEM-768 ⊕ X25519, draft-10 conformant. A keyslot body isct_MLKEM ‖ ct_X ‖ ctx_tag ‖ ct: the shared secret keys the CTX AEAD over the master key with AD"secsec-keyslot-v1" ‖ device_id ‖ le32(gen). Authenticity rests on the caller'smk_commitcheck, not on the wrap. -
Roster (
secsec-roster, §8). An append-only, hash-chained, SSHSIG-signed sigchain anchored by the genesis hash (RFP).foldenforces succession, theprevchain, per-entry signatures, and the currentmk_commiton everyAddDevice, and keepsever_membersso a revoked author's history still verifies. Entries seal as v2 (per-seal random salt, §9.5) and open as v2 or legacy v1 by their FRAME.cold_start_foldpeels the roster keys, opens each entry under the generation its FRAME names, folds, and checks the candidate master key.revoke_closurewalks the add-by tree transitively, collecting grants at or after the revoker's reference point and never the revoker; the data and roster key histories are 64-byte wraps peeled back to generation 1, each data key checked against itsmk_commit. -
Blob store (
secsec-store, §13, §15). One redb file with tables for objects, per-push staging and its activity clock, keyslots, refs, roster entries, and both key histories.stagealways stages (even an id already durable);cas_refpromotes a push's staging and swaps the ref in one transaction;roster_batchapplies a whole sigchain operation atomically under the tip CAS (purging pre-genesis keyslots on genesis and refusing to replace a key-history wrap the chain has rotated past);prune_ifdeletes only when the caller's predicate accepts the live refs and roster length inside the same transaction;reclaim_stagingdrops idle pushes. Clients use the same store as their encrypted object cache. -
Wire and authorization (
secsec-proto, §12, §15, §19). Strict codecs for the hellos, client auth, every request and response, each list count and length bounded before allocation, and write-sidevalidateidentical to the decoder's bounds.op_and_argsmaps a request to its op label,args_hash, and read/write class for both ends.WriteAuthandReadAuthbuild the signed payloads.pruneholdsdead_set_hash,all_heads_hash, andargs_prune;serverholds the §19 limits, token buckets, trailing-window counters, and the per-session quota. -
Snapshot and restore (
secsec-snapshot, §6, §10). Walks the working folder into sealed trees and chunks, reusing path salts from a prior tree (this device's own with the size and nanosecond mtime fast path, or another device's head to seed salts on first contact), skipping symlinks, special files, unsafe names, and restore temps, freezing unreadable or oversized paths at their last synced entry, and carrying unmaterializable entries forward. Restore reconciles the folder to a tree against this device's snapshot (deleting only tracked, unchanged entries, keeping local edits as keep-both copies, writing through fsynced temp files, never following a symlink, and removing a lone macOSIcon\ronly when its folder is going).restore_pathis the explicit-restore overwrite, confined to the synced folder. -
Sync plane (
secsec-sync,secsec-engine, §8.5, §10). The per-ref Head is signed, then sealed under the current generation with a fresh nonce at the generation-stable path/refs/<H>.daggives ancestry, new-commit sets, and lowest common ancestors;mergeis the pure per-path three-way merge with keep-both naming;rollbackholds the gates, the high-water rules, and the sealed frontier.secsec-engineloads a sibling's commit DAG, treats an ancestor sibling as a no-op before the gates, verifies every new commit's signature againstever_members, runs the gates, and either fast-forwards or authors a signed two-parent merge commit against the lowest-id merge base (an empty base, flagged, when that tree is gone). -
Transport (
secsec-transport, §11).PinnedServerVerifieraccepts exactly one SPKI hash;TofuVerifierrecords it only after the TLS 1.3 handshake signature verifies. Both configs pin the cipher suites and X25519 and refuse TLS 1.2; only the client sends keepalives. The application handshake exchanges fixed-size hellos, builds the session transcript, and has the client signsecsec-auth-v1over the TLS exporter,host_id, transcript, and server nonce.framereads length-prefixed messages under a cap, allocating only as bytes arrive;rpc::requestopens one stream per request, reads its challenge, and sends the signed request. -
Server (
secsec-server, §11, §12).serve_connectionruns the handshake under the idle-timeout deadline, checksauthorized_keysand the per-key connection cap, then serves each request stream: a fresh 32-byte challenge, a frame cap chosen by enrollment, andServer::handle, which dispatches the pairing mailbox before the enrollment check, then requires a keyslot (the genesis batch excepted), verifies the per-op signature (a write within 60 s of its challenge), charges rate limits, and executes.authorized_keysis re-parsed when its size or mtime changes and denies when unreadable; the connection re-checks it on the first request after 60 s.admithands the accept loop a server-wide connection slot, freed when its guard drops.reclaimdrops idle staging and idle rate-limit state on the serve loop's timer. -
Client (
secsec-client, §7, §8, §10, §15).repocreates the repository (one genesis batch), cold-starts it against the persisted anchor, peels the data key ring, grants a device, and rotates (with or without a revocation) as one batch, retrying only while the tip or the head moves.pairruns the invite exchange over the mailbox (code MACs undersecsec-pair-mac-v2, length-framed parts).sync_oncefetches and verifies the head, snapshots, and publishes, pulls, merges, or clones, sealing the frontier before any ref-advancing push and returning the base and frontier for the caller to persist in that order.push_objectsstages the new commits and every object outside the remote head's tree.historyserveslogandrestorewith chunks fetched on demand;pruneruns the per-session cache sweep and the count-based retention prune;watcherturns filesystem bursts into one callback;quic::QuicRemoteimplementsRemoteover authorized RPCs. -
CLI (
bin/secsec). Loadssecsec.config(writing the template on first use, clamping on load), resolves the per-folder state directory, takes the folder lock, and drivessync_oncein a loop on watcher events and the poll timer: reconnecting with a verified round trip, probing the sigchain for growth and refolding when it moved, persisting the push id around each round, writing the status file, and running the retention prune once per session.servebinds dual-stack UDP, compacts the store, answers unvalidated sources with a QUIC Retry, enforces the per-IP rate and the server-wide connection cap, and spawnsserve_connectionper admitted connection. -
Desktop UIs and installers (
ui/,install.sh). The GNOME extension and the macOS menu-bar app prompt for the key passphrase, startsecsec sync <folder> --passphrase-stdinwith the passphrase on a pipe, pollsecsec status, and usesecsec stopto stop the sync holding the folder.install.shinstalls the binary with checksum (and, withgh, provenance) verification, the UI or a systemd sync unit for the client, or the server as a systemd user service with lingering (Linux) or a LaunchAgent (macOS).