T-saur

Logo

T-saur (.tsr): a content-addressed, deterministic archive format built for AI agents. Rust, Apache-2.0 OR MIT.

View the Project on GitHub iulianbondari/t-saur

Trust contract for volume descriptors and pieces received from another machine

Status: normative for tsaur volumes serve / fetch (protocol TSXP/1) and for any later peer-to-peer stage. Everything here is about integrity against an expectation the receiver already holds. The current transport (plain TCP, manually supplied addresses) provides no confidentiality and no peer authentication unless the TLS mode of §6 is used, in which case the peer’s identity is a certificate fingerprint the user pinned by hand.

1. Identities involved

Identity Where it comes from What it covers
archive hash archive_b3 BLAKE3 of the whole .tsr file, computed at split time the exact bytes of the archive
set id BLAKE3-derive("tsaur volume set v1", archive_b3 ‖ archive size ‖ piece size ‖ N ‖ M) which archive and which geometry; not the piece hashes, not the archive name
descriptor hash descriptor_b3 BLAKE3 of the CBOR descriptor (stored in every volume’s trailer; printed by split, inspect and fetch) the complete descriptor: set id, archive name, size, archive hash, geometry, every data and parity piece hash, Merkle root
piece hash BLAKE3 of one zero-padded piece, listed in the descriptor the bytes of that piece

None of these says anything about who holds or sends the bytes.

2. How the receiver obtains the expected identity

The expectation must reach the receiver through a channel the receiver trusts, not from the peer it is about to fetch from. Practical channels:

A set id or descriptor hash received from the same peer (for example through SETS) is only a label for asking; it authenticates nothing, because the peer chose both the label and the bytes. fetch therefore refuses to run without --set, and --set/--descriptor values must come from the user, never from the wire.

3. What each option validates

Option What is checked What it proves What it does not prove
--set <id> (required) the received descriptor parses and passes validate(); the set id recomputed from its archive_b3, size, piece size, N, M equals the expected id; the descriptor’s own set_id field equals it the descriptor describes the archive with that hash in that geometry that the piece hashes are the right ones (they are not covered by the set id); who the peer is
--descriptor <hash> (recommended) the received CBOR bytes hash to the expected value (checked before parsing) the entire descriptor is the one the split produced, every piece hash included who the peer is; that the peer will serve anything
neither — — fetch refuses: “trust whatever arrives” is not a mode

With --set only, a peer can hand over a descriptor with the right set id and wrong piece hashes. Pieces matching those wrong hashes are then stored. The receiver is still protected, but later and at a cost: join hashes the rebuilt archive and refuses to give it a name unless it equals archive_b3, which is bound to the set id. Until a join succeeds, everything fetched in set-id-only mode is provisional; the worst case is wasted transfer and disk space, never a wrong archive. The fetch report says which of the two checks was decisive (verified_against), and the CLI prints the provisional-until-join note in set-id-only mode.

When the archive-hash check becomes decisive: at join, always, for every mode, before the output gets its final name. It is the only check that can catch a wrong descriptor in set-id-only mode, and it re-validates everything in descriptor mode as well.

If the receiver already holds any volume of the set locally, the descriptor from that local volume is the reference; a received descriptor is only compared with it, never adopted.

4. What is verified, and when

Step Check On failure
descriptor received size below MAX_DESCRIPTOR before it is read; --descriptor hash when given; CBOR parses; validate() (geometry, counts, piece limit, hash lengths, name length and characters); recomputed set id descriptor discarded, next peer tried, nothing written
piece received announced length equals the piece size (refused before allocation otherwise); BLAKE3 equals the descriptor entry for that (volume, stripe) piece discarded and counted as rejected, next peer tried; nothing written
resume of a partial volume the partial file’s header names this set, index and piece size; every piece the map declares complete is read back and hash-checked; a missing or damaged map means every position is checked; a short file is extended and its missing tail treated as absent pieces that fail are fetched again; a partial file with a foreign header is deleted and started over
volume completed pieces, descriptor and trailer written; renamed from .partial only when every piece has verified partial file and map stay for the next resume
join every piece hash again, then the whole archive against archive_b3 before the final rename temporary output removed, exit code 5 (missing) or 2 (corrupt)

Sender claims are never trusted: the receiver asks for a specific (set id, volume, stripe) and verifies the answer; a HAVE list only decides which peer to ask first. A peer can lie only by sending garbage, which costs it bandwidth and costs the receiver one retry per lie.

The local file names of fetched volumes are derived from the descriptor’s archive name only when that name is a safe single path component (no separators, colons, control characters, reserved device names, trailing dots or spaces); otherwise the set id names the files. Peers cannot choose where the receiver writes.

5. What a hash alone does not authenticate

6. Two transports: plain TCP, and TLS with locally pinned identities

TSXP/1 runs either over plain TCP or inside TLS 1.3. The requests, the replies and every check of §3–§4 are identical in both; the TLS mode adds confidentiality and peer authentication, and nothing else changes.

6.1 Plain mode (default)

On a shared network anyone can read the pieces, impersonate a peer or inject garbage (which the hashes reject). serve therefore listens on loopback by default and requires --expose-lan for anything else, with a printed warning. Integrity yes, secrecy and peer identity no.

6.2 TLS mode with pinned fingerprints

Each instance creates an identity once: tsaur volumes keygen --out peer.key writes an ECDSA P-256 private key (peer.key, PEM, never leaves the machine) and a self-signed certificate (peer.key.crt) and prints the certificate’s fingerprint (SHA-256 of the DER certificate, 64 hex characters). Nothing in the certificate is trusted except that fingerprint: names, validity dates and issuer are ignored on purpose, because there is no authority to vouch for them. The TLS stack is rustls (ring provider); T-saur adds only the two pinning verifiers.

Three separate properties decide what a client on the network gets, and serve never implies any of them: encryption (is the path readable by others?), server identity (can the client verify whom it talks to?) and client authorization (who may fetch at all?).

serve form (a non-loopback --listen always needs --expose-lan) Encryption Server identity Client authorization
serve on loopback (default) none none processes on this machine only
serve --expose-lan --allow-anyone none none anyone who can reach the port
serve --expose-lan --tls-identity s.key --allow-anyone TLS 1.3 yes: clients pin its fingerprint with --peer-id anyone who can reach the port
serve --expose-lan --tls-identity s.key --allow <fp> ... TLS 1.3 yes the listed client fingerprints only (mutual TLS)
serve --expose-lan --tls-identity s.key --allow-set <set id>=<fp> ... (also --allow-file) TLS 1.3 yes listed fingerprints, each for its own sets (mutual TLS); a set outside a client’s lists is answered like an unknown set (§6.4)

--expose-lan alone is refused: who may fetch has to be named, either with --allow (client fingerprints, needs --tls-identity) or with --allow-anyone. Anonymous access is therefore always an explicit choice; the server states the resulting access on stdout (access: line, access object in --json) and warns on stderr when clients are anonymous. On the fetching side, fetch ... --from A:port --peer-id <A's fingerprint> [--tls-identity client.key] pins A’s fingerprint (one --peer-id per --from, in the same order) and presents the client’s own identity when it has one; without --peer-id the connection is plain.

Rules that follow from the pinning:

6.3 Revocation

--revoke FILE names a text file with one certificate fingerprint per line (64 hex characters; colons and letter case tolerated; # starts a comment; blank lines ignored; at most 1 MiB, so a wrong path fails fast). serve and fetch read it once at start-up (restart to apply a change) and refuse every fingerprint in it:

6.4 Which client may read which set

--allow <fp> admits a client to every served set. --allow-set <set id>=<fp> (repeatable; the set id may be a prefix of at least 16 hex characters that matches exactly one served set, resolved before anything is bound) and --allow-file FILE (one rule per line, <fingerprint> [set id ...], no set id = every set) admit a client to specific sets only. The handshake admits the union of all lists (minus revocations); after it, the server reads the client’s verified certificate and consults the lists for every request: SETS lists only the sets the client may read, and DESCRIPTOR, HAVE and PIECE for another set are answered exactly like an unknown set (ERR 404 unknown set), so a client learns nothing about sets it may not read, not even that they exist. The 404 was chosen over a silent close on purpose: a close is what the client treats as “busy” and retries sixteen times, which would turn an authorization failure into a slow one. Per-set lists need mutual TLS (--tls-identity) and exclude --allow-anyone; the start-up listing shows readers: all | N per set so an operator can check the mapping without exposing fingerprints.

What is still missing after this step: more than two participants per fetch, measurements on two real devices (TWO-DEVICE-BENCHMARK-PLAN.md), and an independent review of the whole exchange path by another evaluator (docs/review/REVIEW-PACKAGE.md, ROADMAP.md).