PEER_AUTH_PLAN simplified drastically. GPT 5.6 Sol (ultra) was doing a great job, but with my poor prompting it completely overengineered.
This commit is contained in:
+471
-1305
@@ -1,460 +1,271 @@
|
|||||||
# Peer authentication, identity continuity, and download-source authorization
|
# Pragmatic LAN safety, peer identity, and content integrity
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Architecture approved; implementation plan.
|
Revised implementation plan; not yet implemented.
|
||||||
|
|
||||||
This document incorporates the prior holistic review and subsequent review
|
This plan deliberately treats Lanspread as what it is: a desktop utility for
|
||||||
dialogue. It replaces the previous version of this file and
|
friends and other attendees at a LAN party to discover each other, share a
|
||||||
supersedes the statement in `CALL_TO_PLAY_FIXES_PLAN.md` that cryptographic
|
known game catalog at LAN speed, and coordinate a match. It is not an account
|
||||||
peer identities should not be introduced.
|
system, a global untrusted file-sharing network, or a device-administration
|
||||||
|
product.
|
||||||
|
|
||||||
Nothing in this document is implemented merely because it is specified here.
|
The normal user journey must remain:
|
||||||
Each phase below has its own implementation and acceptance gate. Phase 1a
|
|
||||||
scaffolding can proceed once this plan is accepted; persistent identity writes
|
|
||||||
wait for the identity transition table in §7.2 to be encoded in tests, and the
|
|
||||||
network runtime does not switch from the legacy UUID to the derived PeerId
|
|
||||||
until Phase 2's protocol bump.
|
|
||||||
|
|
||||||
The project has one current wire version and no compatibility paths. Every
|
1. Open Lanspread.
|
||||||
wire-changing phase bumps `PROTOCOL_VERSION`; peers on any other version are
|
2. See nearby people and their available games automatically.
|
||||||
rejected.
|
3. Click Download or Stream Install without approving every source.
|
||||||
|
4. Let Lanspread swarm from matching peers and verify the result itself.
|
||||||
|
5. Use Call to Play while those people are present.
|
||||||
|
|
||||||
## 1. Executive summary
|
Security mechanisms in this plan are automatic. There are no key backup
|
||||||
|
dialogs, trust ceremonies, fingerprint prompts, or per-device download
|
||||||
|
permissions in the normal UI.
|
||||||
|
|
||||||
The design has six connected parts:
|
The project still has one current wire version and no compatibility shims. The
|
||||||
|
wire changes below are developed together and activated with one protocol bump,
|
||||||
|
not three partially compatible protocol generations.
|
||||||
|
|
||||||
1. Each Lanspread runtime has a long-lived Ed25519 identity. `PeerId` is
|
## 1. Product and architecture decisions
|
||||||
derived from the public key; it is never a caller-supplied UUID.
|
|
||||||
2. Every outbound connection takes a `PeerEndpoint { peer_id, addr }`. TLS
|
|
||||||
pins the responder to that identity and performs real TLS 1.3
|
|
||||||
CertificateVerify validation. An address is never used to discover an
|
|
||||||
identity after connecting.
|
|
||||||
3. mDNS and inbound `Hello` messages are candidate hints. They cannot mutate
|
|
||||||
authenticated peer state. Listener ownership is proven by a bounded pinned
|
|
||||||
connect-back.
|
|
||||||
4. Control requests and responses use exact signed envelopes. Independently
|
|
||||||
relayed Call-to-Play events carry their own signatures, creator commitment,
|
|
||||||
and content-derived IDs.
|
|
||||||
5. User-visible continuity, blocking, and permission to download from a device
|
|
||||||
are separate trust decisions. New devices are not remote byte sources until
|
|
||||||
explicitly allowed.
|
|
||||||
6. Transfer-authoritative manifests and bytes come only from allowed,
|
|
||||||
responder-pinned identities. This is source authorization, not content
|
|
||||||
integrity: an allowed malicious device can still send hostile same-sized
|
|
||||||
bytes until catalog-owned hashes or signatures exist.
|
|
||||||
|
|
||||||
There is also one independent prerequisite. The current download preparation
|
| Area | Decision | User-visible result |
|
||||||
path can truncate another game's `local/` data from an attacker-supplied file
|
|
||||||
description before any peer connection occurs. §6 specifies a standalone fix
|
|
||||||
that lands before persistent/runtime identity work.
|
|
||||||
|
|
||||||
## 2. Current security and correctness facts
|
|
||||||
|
|
||||||
- The repository-wide `cert.pem` and `key.pem` authenticate only “some build of
|
|
||||||
Lanspread.” Their private key is public, so they do not authenticate a peer.
|
|
||||||
- The current `peer_id` is a self-asserted UUID. mDNS, `Hello`, request bodies,
|
|
||||||
peer-table rebinding, liveness, and Call-to-Play authority ultimately trust
|
|
||||||
that string.
|
|
||||||
- `peer_db` can evict an existing address owner during a claimed collision.
|
|
||||||
Removing only that eviction branch would corrupt the `peers`/`addr_index`
|
|
||||||
invariant; the replacement must reject conflicts atomically.
|
|
||||||
- `handshake.rs` currently records advertised listener addresses rather than
|
|
||||||
consistently retaining the endpoint actually reached.
|
|
||||||
- Download, retry, streamed-install, healing, and direct-CLI paths sometimes
|
|
||||||
retain only an address or fabricate an address-derived peer ID. That is
|
|
||||||
incompatible with responder pinning.
|
|
||||||
- A custom rustls `ServerCertVerifier` has two separate duties: inspect and pin
|
|
||||||
the presented public key, and cryptographically verify TLS 1.3
|
|
||||||
CertificateVerify. Returning `HandshakeSignatureValid::assertion()` without
|
|
||||||
the second operation lets an attacker replay another peer's public
|
|
||||||
certificate with the attacker's private key.
|
|
||||||
- Call-to-Play snapshots are relayed. Authenticating the relay does not prove
|
|
||||||
the authorship of third-party events.
|
|
||||||
- Current terminal compaction intentionally retains session-long tombstones,
|
|
||||||
but rootless tombstone handling loses the selected terminal's order key and
|
|
||||||
fresh stores cannot ingest such tombstones correctly. Signed events must fix
|
|
||||||
that replication state, not claim replication remains untouched.
|
|
||||||
- The current active-history capacity tests let terminal actions reduce state,
|
|
||||||
but there is no hard total tombstone/byte bound and no reserve for ordinary
|
|
||||||
local publication.
|
|
||||||
- `LengthDelimitedCodec::new()` already has an 8 MiB default maximum; it is not
|
|
||||||
unbounded. Protocol-specific frame and batch limits still need to be explicit
|
|
||||||
and lower where the bounded payload permits.
|
|
||||||
- The GUI uses Tauri's `app_data_dir()`, not necessarily `~/.lanspread`.
|
|
||||||
Update and uninstall/reinstall persistence therefore depends on the platform
|
|
||||||
and packaging behavior.
|
|
||||||
- `prepare_game_storage` currently receives the whole games directory and may
|
|
||||||
call `create(true).truncate(true)` for a cross-game or protected `local/`
|
|
||||||
path before a source connection is attempted. This is a live data-loss bug,
|
|
||||||
independent of peer authentication.
|
|
||||||
|
|
||||||
## 3. Goals
|
|
||||||
|
|
||||||
1. Give every runtime a stable cryptographic identity whose public ID is
|
|
||||||
derived, not asserted.
|
|
||||||
2. Preserve that identity across normal updates and address changes, while
|
|
||||||
making storage failures and platform-dependent reinstall behavior honest.
|
|
||||||
3. Authenticate every selected responder and prove possession of its TLS
|
|
||||||
private key.
|
|
||||||
4. Remove address-only identity throughout the peer, metadata, download,
|
|
||||||
retry, streamed-install, healing, liveness, and CLI paths.
|
|
||||||
5. Attribute every state-changing control message to a verified key.
|
|
||||||
6. Make stored and forwarded Call-to-Play events independently verifiable.
|
|
||||||
7. Prevent a blocked author from regaining influence through an allowed relay.
|
|
||||||
8. Bound peer/authentication and Call-to-Play work without evicting permanent
|
|
||||||
anti-resurrection state.
|
|
||||||
9. Default remote download sources to user approval, while leaving public
|
|
||||||
browsing and outbound serving appropriate for a LAN party.
|
|
||||||
10. Expose identity continuity, new-key/name conflicts, blocking, source
|
|
||||||
permission, overload, and repair states clearly in the UI.
|
|
||||||
11. Land each negative security scenario with the phase that creates its trust
|
|
||||||
boundary.
|
|
||||||
|
|
||||||
## 4. Non-goals and accepted limits
|
|
||||||
|
|
||||||
- No PKI, accounts, CA, or internet service. Trust is self-certifying identity,
|
|
||||||
first-seen continuity, and explicit local policy.
|
|
||||||
- A key proves continuity, not a human name. “Alice” remains a signed
|
|
||||||
self-assertion; the UI shows the fingerprint and warns when a familiar name
|
|
||||||
appears under a new key.
|
|
||||||
- Authentication does not exclude a hostile guest. A fresh key is a valid new
|
|
||||||
identity. Default-deny source permission adds a human authorization step but
|
|
||||||
does not make that device's files trustworthy.
|
|
||||||
- No complete game-content integrity in this plan. Catalog-owned hashes or
|
|
||||||
signatures remain separate urgent work.
|
|
||||||
- No durable, global replay ledger. TLS prevents passive capture, recipient
|
|
||||||
binding prevents cross-peer reuse, and a bounded in-process nonce set
|
|
||||||
suppresses duplicates. Operations must still be idempotent or sequenced.
|
|
||||||
- No receiver-relative age expiry for stored signatures. Historical events and
|
|
||||||
session tombstones remain valid when relayed later.
|
|
||||||
- No Byzantine convergence guarantee after adversarial global capacity
|
|
||||||
exhaustion. Call-to-Play memory and merge work remain bounded; remote
|
|
||||||
availability and exact convergence may stop until the user
|
|
||||||
blocks/quarantines authors and resets or restarts party state.
|
|
||||||
- No comprehensive accept-connection/control-stream/disk-read/serve-operation
|
|
||||||
scheduler is added here. A hostile LAN host can still exhaust application
|
|
||||||
tasks below physical link saturation through anonymous public serving in the
|
|
||||||
intermediate phases. This is an explicit availability risk for separate
|
|
||||||
server-hardening work, not a guarantee hidden behind T9; the concrete new
|
|
||||||
amplification surfaces introduced by this plan remain bounded.
|
|
||||||
- No anonymity or metadata privacy. Peer IDs, keys, names, and library summaries
|
|
||||||
are public to the local segment by design.
|
|
||||||
- No promise that uninstall/reinstall preserves a file-backed identity on
|
|
||||||
every platform. The UI and docs describe the actual package behavior and
|
|
||||||
recommend an encrypted backup.
|
|
||||||
|
|
||||||
## 5. Threat model and resulting guarantees
|
|
||||||
|
|
||||||
The attacker is on the same L2 segment, can send arbitrary packets, run a
|
|
||||||
modified build, create many identities, and knows the repository's current TLS
|
|
||||||
private key. The attacker does not have code execution or OS-secret access on a
|
|
||||||
victim.
|
|
||||||
|
|
||||||
| # | Attack | Result after the owning phase |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| T1 | Impersonate a selected responder | Requires the responder's private key. During Phase 2, inbound initiators are not authenticated, so their claims cannot mutate state. |
|
| Filesystem safety | Validate the complete destination manifest before any mutation and confine it to one catalog game root. | A hostile peer cannot overwrite another game, `local/`, saves, or transaction state. |
|
||||||
| T2 | MITM an outbound QUIC connection | Fails responder pinning and/or TLS 1.3 CertificateVerify. |
|
| Content authority | Ship SHA-256 file and chunk hashes from the same bundled catalog authority as `game.db`. | Every eligible nearby peer is usable automatically; wrong bytes are rejected and retried elsewhere. |
|
||||||
| T3 | Evict a peer remotely | `Goodbye` is removed. Conditional authenticated liveness is the only remote-removal path. |
|
| Peer identity | Use one installation-local TLS key and derive `PeerId` from that TLS public key. | Identity works silently and survives ordinary restarts when possible; users do not manage it. |
|
||||||
| T4 | Rebind or collide a listener address | mDNS and advertised addresses are hints. Only a successful pinned dial binds an endpoint; a cross-ID collision is rejected without eviction. |
|
| Transport | Pin every outbound QUIC connection to the expected `PeerId`. | An address spoof or MITM cannot impersonate the peer selected as a source. |
|
||||||
| T5 | Start, cancel, or extend another creator's call | Creator actions require the `CallRef.creator_key` signature. |
|
| Control messages | Use ordinary bounded protocol messages inside TLS. Treat unauthenticated inbound change notifications only as hints that trigger a pinned pull. | No signed-envelope layer, nonce ledger, or message-signing overhead. |
|
||||||
| T6 | Chat or RSVP as another participant | Participant actions require that participant's signature and a valid call root. |
|
| Call to Play | Exchange only each peer's own session state by direct pinned pulls; do not relay third-party histories. | Calls are live LAN-party state and disappear naturally as their authors leave. |
|
||||||
| T7 | Reuse a captured control object | TLS prevents passive capture, local-recipient/context checks prevent cross-peer reuse, request/response correlation and an atomic bounded nonce set suppress duplicates. No durable universal replay guarantee is claimed. |
|
| Privacy | Provide one global Local network sharing switch. | Participation is easy to understand; no per-peer policy matrix. |
|
||||||
| T8 | Exhaust Call-to-Play state | One key cannot exceed its count/byte quota and remote traffic cannot consume the local reserve. Sybil keys can fill the bounded remote pool; overload is surfaced and availability/convergence are then explicitly not guaranteed. |
|
| Protocol rollout | Make one cutover to the new current protocol. | Mixed versions are explained clearly, without maintaining legacy paths. |
|
||||||
| T9 | Flood candidate/state admission | Candidate, live-peer, trust, nonce, and connect-back work/state are bounded and deduplicated. Generic accept-loop/link saturation remains an explicit availability limit. |
|
|
||||||
| T10 | Reuse a familiar display name under a new key | The UI retains the old pin, marks the key as new, and shows a name conflict. It does not claim the name is verified. |
|
|
||||||
| T11 | Serve hostile game bytes | An unapproved identity cannot become a manifest or byte source. An explicitly allowed malicious identity can still serve hostile content; catalog integrity is unsolved. |
|
|
||||||
| T12 | Steal a local identity | OS secret storage is preferred; file and explicit-file modes are labelled honestly. Local compromise remains out of scope. |
|
|
||||||
| T13 | Destroy another game's protected files through a manifest | The standalone manifest-confinement fix rejects the complete manifest before any filesystem or `version.ini` transaction mutation. |
|
|
||||||
|
|
||||||
## 6. Prerequisite: confine download preparation before persistent identity work
|
The resulting data flow is intentionally small:
|
||||||
|
|
||||||
This is a standalone safety fix, not a protocol or cryptography change, and it
|
```text
|
||||||
lands before Phase 1b persistent identity work; Phase 1a may proceed in
|
mDNS candidate -> responder-pinned TLS -> peer-owned snapshot or file bytes
|
||||||
parallel.
|
|
||||||
|
|
||||||
### 6.1 Validated manifest boundary
|
bundled content manifest -> validated local download plan
|
||||||
|
-> SHA-256 check for every received chunk
|
||||||
|
-> version.ini commit only after complete success
|
||||||
|
|
||||||
|
local Call to Play change -> cheap invalidation hint to known peers
|
||||||
|
-> each peer pulls the author's current state over pinned TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Threat model and guarantees
|
||||||
|
|
||||||
|
Assume a hostile device can join the same LAN, advertise arbitrary mDNS data,
|
||||||
|
send arbitrary protocol messages, occupy reused IP addresses, and run a
|
||||||
|
modified Lanspread build. The attacker does not control the victim's OS, the
|
||||||
|
installed Lanspread application, or its bundled catalog files.
|
||||||
|
|
||||||
|
After this plan:
|
||||||
|
|
||||||
|
- a remote description cannot make Lanspread create, truncate, or delete a
|
||||||
|
path outside the requested catalog game's download-owned area;
|
||||||
|
- a selected responder must prove possession of the TLS private key whose
|
||||||
|
public key derives the expected `PeerId`;
|
||||||
|
- mDNS, IP addresses, display names, and inbound notification bodies never
|
||||||
|
become identity authority by themselves;
|
||||||
|
- a source cannot make a download commit bytes that differ from the hashes in
|
||||||
|
the victim's bundled catalog, even if that source is the only peer present;
|
||||||
|
- corrupt sources are removed from the current transfer automatically rather
|
||||||
|
than presented to the user as a trust decision; and
|
||||||
|
- one peer cannot publish Call-to-Play actions as another peer or mutate
|
||||||
|
another peer's author-owned state.
|
||||||
|
|
||||||
|
The following are explicit non-goals:
|
||||||
|
|
||||||
|
- A `PeerId` does not prove a human name. Display names remain friendly labels.
|
||||||
|
- The installation key is not a user account and has no promised continuity
|
||||||
|
across OS reinstall, application-data deletion, or copying the application
|
||||||
|
to another computer.
|
||||||
|
- Content hashes prove that bytes match the bundled catalog. They do not prove
|
||||||
|
that the catalog publisher's game is benign, licensed, or malware-free.
|
||||||
|
- A hash advertised by the same peer that sends the bytes is not trusted. The
|
||||||
|
expected hash must come from the local bundled catalog.
|
||||||
|
- When Local network sharing is enabled, nearby devices may browse and request
|
||||||
|
shared catalog content. Per-device admission and requester blocking are not
|
||||||
|
part of this product model.
|
||||||
|
- Basic frame, connection, and work limits are required, but internet-scale
|
||||||
|
Sybil resistance and Byzantine convergence are not goals for a LAN-party
|
||||||
|
utility.
|
||||||
|
- Peers on another protocol version do not interoperate. The UI explains the
|
||||||
|
mismatch instead of adding a legacy protocol path.
|
||||||
|
|
||||||
|
## 3. Normative design
|
||||||
|
|
||||||
|
### 3.1 Confine download preparation first
|
||||||
|
|
||||||
|
This remains the first implementation task because it fixes a live local
|
||||||
|
data-loss path without depending on authentication or a wire change.
|
||||||
|
|
||||||
The peer core constructs a `ValidatedDownloadManifest` before
|
The peer core constructs a `ValidatedDownloadManifest` before
|
||||||
`begin_version_ini_transaction`, `prepare_game_storage`, directory creation,
|
`begin_version_ini_transaction`, `prepare_game_storage`, directory creation,
|
||||||
file open/truncate/resize, or any other filesystem mutation. Storage accepts
|
file creation/truncation, preallocation, or cleanup. Storage functions accept
|
||||||
only that validated type; neither Tauri nor a remote peer can pass raw
|
that validated type, never raw `GameFileDescription` values from Tauri or a
|
||||||
`GameFileDescription` values to it.
|
peer.
|
||||||
|
|
||||||
For the entire list, validation must:
|
Validation is for the complete list and fails without any mutation. It must:
|
||||||
|
|
||||||
- require every descriptor's `game_id` to equal the requested game;
|
- require a known catalog `game_id` and resolve every destination relative to
|
||||||
- resolve destinations relative to exactly `<games_folder>/<game_id>`, not the
|
exactly `<games_folder>/<game_id>`;
|
||||||
whole games directory;
|
- use one canonical forward-slash relative-path form and reject empty,
|
||||||
- at this standalone no-version-bump boundary, leave protocol-7 producer bytes
|
absolute, drive-qualified, UNC, NUL, `.`, `..`, mixed-separator, and
|
||||||
unchanged and normalize its current platform all-`/` or all-`\\` separator
|
non-normalized paths;
|
||||||
form exactly once before validation; reject mixed
|
- reject duplicate paths, file/directory conflicts, and platform aliases such
|
||||||
separators and ambiguous/empty components. Accept and discard only the
|
as Windows case, trailing-dot/space, device-name, and alternate-data-stream
|
||||||
current exact redundant root descriptor
|
collisions;
|
||||||
`{ relative_path: game_id, is_dir: true, size: 0 }`; reject every other empty
|
- reject `local/`, `.local.*`, download/install intent state, legacy state,
|
||||||
game-relative entry. All absolute, drive-qualified, UNC, NUL, `.`/`..`,
|
scratch sentinels, and every other path owned by installation or recovery,
|
||||||
parent, cross-game, and non-normalized results are rejected;
|
while allowing the intended root `version.ini`;
|
||||||
- reject duplicate normalized paths and conflicting file/directory shapes;
|
- require the game root to be one direct non-symlink child of the configured
|
||||||
- require directory size to be zero and exactly one regular root
|
games directory and avoid following symlink or reparse components while
|
||||||
`version.ini` where the existing transaction requires it;
|
opening destinations;
|
||||||
- use one shared `is_reserved_game_path` policy in scanning, serving,
|
- enforce descriptor-count, individual-size, and aggregate-size limits; and
|
||||||
preparation, and discard. It rejects `local/`, `.local.*`, `.sync`,
|
- require the exact root/file shape needed for a complete downloadable game,
|
||||||
`.lanspread/`, `.lanspread.json`, `.softlan_game_installed`,
|
including one regular root `version.ini`.
|
||||||
`.version.ini.tmp`, `.version.ini.discarded`, and all current
|
|
||||||
version-transaction temporary/discard paths while allowing the one intended
|
|
||||||
root `version.ini`;
|
|
||||||
- on Windows, compare duplicates and reserved names case-insensitively and
|
|
||||||
reject alternate-data-stream colons, DOS device components, and trailing-dot
|
|
||||||
or trailing-space aliases before filesystem lookup;
|
|
||||||
- require the game root to be a direct non-symlink child of the games folder
|
|
||||||
and avoid following a symlink or reparse component at the destination; and
|
|
||||||
- enforce explicit descriptor-count, per-file-size, and aggregate-size bounds
|
|
||||||
chosen from real catalog measurements in Phase 0. Phase 2b also enforces
|
|
||||||
those bounds on raw remote manifests before retaining them in `peer_db`,
|
|
||||||
consensus state, or Tauri output.
|
|
||||||
|
|
||||||
The UI supplies only `{ game_id }`; this plan does not support partial game
|
The Tauri command supplies only the selected `game_id`. The peer core chooses
|
||||||
selection. The peer core selects the complete backend-authoritative manifest,
|
the complete authoritative plan. A UI-echoed file list is never authority.
|
||||||
and only a complete successfully written payload may commit `version.ini` or
|
|
||||||
become installable. A UI-echoed description is never authority. If partial
|
|
||||||
downloads are introduced later, they need an explicit dependency closure and
|
|
||||||
must not commit the installable sentinel.
|
|
||||||
|
|
||||||
Before committing `version.ini`, transactionally remove or quarantine every
|
After a successful complete transfer, remove download-owned files absent from
|
||||||
download-owned, nonreserved path under the requested game that is absent from
|
the authoritative manifest before committing `version.ini`. Preserve
|
||||||
the complete selected manifest. Preserve `local/` and all reserved transaction
|
`local/`, install staging/backup state, and user-owned files in all success,
|
||||||
state. This prevents a stale `.eti` from an older, failed, revoked, or
|
failure, cancellation, and recovery paths.
|
||||||
unapproved transfer from being installed or served alongside the newly
|
|
||||||
approved payload.
|
|
||||||
|
|
||||||
### 6.2 Mandatory proof
|
For the current protocol, this validator safely contains the existing remote
|
||||||
|
descriptions. A narrow protocol-7 adapter requires and removes exactly one
|
||||||
|
matching leading `game_id/` component (and discards only the current exact
|
||||||
|
redundant game-root directory entry) before constructing root-relative paths;
|
||||||
|
it rejects a missing/different/doubled prefix. After the protocol cutover, the
|
||||||
|
same validated type is constructed directly from the bundled content manifest
|
||||||
|
and remote descriptions cease to define local paths at all.
|
||||||
|
|
||||||
Tests put sentinel bytes in `OtherGame/local/save.dat` and the requested
|
Required proof includes hostile descriptors placed after valid descriptors,
|
||||||
game's `local/`, then submit malicious descriptors and prove that no existing
|
cross-game paths, both requested and other-game `local/` sentinels, reserved
|
||||||
file changes and no new path is created. They also cover a valid first
|
paths, duplicates and aliases, symlink/reparse destinations, oversized lists,
|
||||||
descriptor followed by an invalid later descriptor, traversal/UNC/drive and
|
and stale download-owned files. Every rejection must prove zero filesystem
|
||||||
case variants, reserved paths, duplicates, root-shape errors, game-ID mismatch,
|
mutation.
|
||||||
and symlink/reparse destinations.
|
|
||||||
They also seed an unlisted download-owned `stale.eti`, complete an authoritative
|
|
||||||
download, and prove that it is gone before installation/serving while every
|
|
||||||
protected `local/` sentinel remains.
|
|
||||||
|
|
||||||
The gate is `just fmt`, `just clippy`, `just test`, `just build`, targeted
|
### 3.2 Make the bundled catalog the content authority
|
||||||
download/hostile-descriptor peer-CLI scenarios during development, and the
|
|
||||||
unfiltered `just peer-cli-tests` before completion.
|
|
||||||
|
|
||||||
## 7. Normative design
|
`game.db` is already the application's authority for game identity and
|
||||||
|
version. Add a reproducibly generated companion catalog artifact, for example
|
||||||
|
`content-manifests-v1.json`, and package it with both the desktop application
|
||||||
|
and peer-CLI fixtures.
|
||||||
|
|
||||||
### 7.1 Identity primitives, IDs, and crate graph
|
For each supported `(game_id, game_version)`, the artifact contains:
|
||||||
|
|
||||||
- Identity algorithm: Ed25519 using the existing AWS-LC cryptographic stack.
|
|
||||||
- `PeerId` is lowercase RFC 4648 base32 without padding of the full
|
|
||||||
`SHA-256(raw_ed25519_public_key)` digest: 52 fixed ASCII characters. It fits a
|
|
||||||
DNS label and retains 128-bit generic collision strength.
|
|
||||||
- UI fingerprints group the full ID for copying and show an unambiguous short
|
|
||||||
prefix only alongside a display name. Protocol policy never compares short
|
|
||||||
IDs.
|
|
||||||
- `lanspread-proto` owns dumb wire types and exact transcript builders:
|
|
||||||
`PeerId`, `PublicKey([u8; 32])`, `Signature([u8; 64])`,
|
|
||||||
`Nonce([u8; 16])`, `CallNonce([u8; 32])`, signed envelope/event types, and
|
|
||||||
explicit base64url serde adapters. It has no crypto or storage dependency.
|
|
||||||
- `lanspread-identity` depends on `lanspread-proto` and owns derivation,
|
|
||||||
signing/verifying, secret storage, and TLS identity material.
|
|
||||||
- `lanspread-peer` depends on both. `lanspread-proto` never depends on
|
|
||||||
`lanspread-identity`; there is no contradictory two-way dependency.
|
|
||||||
- Key, signature, nonce, and opaque payload JSON fields use
|
|
||||||
URL-safe base64 without padding and exact decoded lengths. They must not use
|
|
||||||
`Bytes`' default JSON numeric-array representation.
|
|
||||||
- Core code constructs all author, call, event, and message IDs. Frontend and
|
|
||||||
CLI commands express an action against a selected call; they do not supply
|
|
||||||
trusted identity or event-ID fields.
|
|
||||||
|
|
||||||
### 7.2 Identity storage is a state machine, not a fallback chain
|
|
||||||
|
|
||||||
#### Explicit modes
|
|
||||||
|
|
||||||
CLI flags take precedence over environment variables. Seed and file modes are
|
|
||||||
mutually exclusive; conflicting settings return a typed configuration error.
|
|
||||||
|
|
||||||
| Mode | Rule |
|
|
||||||
|---|---|
|
|
||||||
| `--identity-seed` or `LANSPREAD_IDENTITY_SEED` | Require one exact valid seed, never persist it, and do not read or modify default keyring/file/sidecar state. |
|
|
||||||
| `--identity-file` or `LANSPREAD_IDENTITY_FILE` | The named versioned secret file is the sole authority for that run. Missing, inaccessible, or corrupt means failure; creation requires a separate explicit operation. No fallback. |
|
|
||||||
| No explicit mode | Use the persistent-store table below. |
|
|
||||||
|
|
||||||
The versioned secret record contains at least `{ version, seed, created_at }`.
|
|
||||||
The reconstructible non-secret `<state_dir>/identity.json` contains
|
|
||||||
`{ version, peer_id, public_key, created_at, backend }`. Its ID and key are
|
|
||||||
always recomputed from the secret before use.
|
|
||||||
|
|
||||||
Persistent backend observations normalize to:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Present(secret) | NoEntry | Locked | Denied | Unavailable | Corrupt
|
CatalogContentManifest {
|
||||||
|
schema_version
|
||||||
|
game_id
|
||||||
|
game_version
|
||||||
|
chunk_size
|
||||||
|
files: [
|
||||||
|
{ canonical_path, kind, size, file_sha256, chunk_sha256[] }
|
||||||
|
]
|
||||||
|
streamed_install_files: [
|
||||||
|
{ canonical_path, kind, size, file_sha256 }
|
||||||
|
]
|
||||||
|
content_id
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Only `NoEntry` proves absence. An I/O error, locked store, denied access,
|
Entries are sorted by canonical path. `content_id` is SHA-256 over a
|
||||||
unavailable service, or corrupt record never authorizes generation.
|
versioned, length-delimited encoding of all preceding manifest fields and
|
||||||
|
hashes, excluding the `content_id` field itself; it is not the current
|
||||||
|
noncryptographic `u64 manifest_hash`. Golden tests freeze that encoding. The
|
||||||
|
ordinary chunk size initially matches Lanspread's existing 128 MiB transfer
|
||||||
|
chunk so verification does not create a second chunking scheme.
|
||||||
|
|
||||||
#### Persistent transition table
|
The catalog publishing workflow must generate these manifests from the
|
||||||
|
canonical game packages, verify them by rereading the packages, and fail the
|
||||||
|
application build/release if a downloadable catalog entry lacks one. Runtime
|
||||||
|
peer consensus and “the only peer said this hash” are not substitutes for this
|
||||||
|
artifact. If real package inputs are unavailable during development, fixture
|
||||||
|
manifests may prove the code path, but the phase is not complete for production
|
||||||
|
games.
|
||||||
|
|
||||||
| Sidecar | Backend observations | Required action |
|
Peers advertise only that they can serve a catalog `content_id`. A peer counts
|
||||||
|---|---|---|
|
as a source for the local catalog game only when its advertised ID exactly
|
||||||
| Valid | Selected backend is `Present` and derived key/ID match | Load only that backend; do not probe or switch. |
|
matches the receiver's expected ID. The receiver builds paths, sizes, chunks,
|
||||||
| Valid | Selected backend is missing, locked, denied, unavailable, corrupt, or mismatched | Stop the peer runtime and enter the corresponding typed repair state. Never fall through or generate. |
|
and expected hashes entirely from its local catalog. This replaces remote
|
||||||
| Missing | Exactly one readable secret exists and every other supported backend conclusively reports `NoEntry` | Reconstruct the sidecar from that secret and load it. |
|
manifest selection and majority-by-file-size consensus.
|
||||||
| Missing | More than one secret exists, even if identities match | Enter repair and require an explicit authoritative-backend choice. |
|
|
||||||
| Missing | Readable secrets derive different identities | `AmbiguousIdentity`; show backend names and public fingerprints, never seed material. |
|
|
||||||
| Missing | No secret exists and every supported backend conclusively reports `NoEntry` | Fresh install: create in the preferred keyring backend. |
|
|
||||||
| Missing | Any backend is locked, denied, unavailable, or corrupt | Enter repair; do not generate or fall through. |
|
|
||||||
| Corrupt | Exactly one readable supported-version secret exists and all others are conclusively absent | Reconstruct the derivative sidecar and load. |
|
|
||||||
| Corrupt | Otherwise | Enter repair. Even all-`NoEntry` does not silently generate because the sidecar proves prior state existed. |
|
|
||||||
| Unsupported sidecar or secret version | Any | Return `UnsupportedVersion` without probing beyond what identified the version and without writing anything. An older binary never reconstructs or overwrites newer state. |
|
|
||||||
|
|
||||||
On a genuinely fresh Linux system without Secret Service, the GUI may
|
For ordinary downloads:
|
||||||
explicitly offer “Create a file-backed identity.” Headless use chooses a seed
|
|
||||||
or explicit identity file. A failed keyring write is followed by a read probe;
|
|
||||||
it never immediately falls through because the write may have partly
|
|
||||||
succeeded.
|
|
||||||
|
|
||||||
The supported default keyring locator—service `network.paul.lanspread`, account
|
1. Select every currently reachable peer advertising the expected
|
||||||
`peer-identity`—means one default identity per OS account. Multi-profile,
|
`content_id`; there is no approval prompt.
|
||||||
container, and simultaneous test identities use explicit files/seeds. If that
|
2. Carry `PeerEndpoint { peer_id, addr }` and `content_id` through planning,
|
||||||
scope changes, Phase 0 must first define a deterministic, recoverable locator;
|
swarming, progress, and retry.
|
||||||
two stores must not silently implement different identity scopes.
|
3. The sender serves only an exact catalog file/range for the requested
|
||||||
|
`(game_id, content_id)` and applies the same canonical/reserved-path policy
|
||||||
|
before opening a local file. A caller-supplied path can never expose
|
||||||
|
`local/` or another local file.
|
||||||
|
4. Hash each chunk while receiving it and compare it before marking that chunk
|
||||||
|
complete. Exact length, offset coverage, and the catalog file shape are also
|
||||||
|
mandatory.
|
||||||
|
5. On mismatch, invalidate that write, quarantine that `(PeerId, content_id)`
|
||||||
|
for the current runtime/transfer, and retry the chunk from another matching
|
||||||
|
peer. Do not create durable “trust” state.
|
||||||
|
6. Commit `version.ini` only after every catalog entry and chunk has completed
|
||||||
|
successfully. Failure leaves the game non-downloadable/non-installable and
|
||||||
|
preserves `local/`.
|
||||||
|
|
||||||
#### Locking and durability
|
Existing downloaded payloads are verified against the catalog in the
|
||||||
|
background before they are newly advertised or installed, with results cached
|
||||||
|
against the existing file fingerprint. The user sees ordinary “Verifying game
|
||||||
|
files” progress, not a security decision.
|
||||||
|
|
||||||
- The application identity session acquires an exclusive OS advisory lease
|
Streamed install needs a catalog-owned extracted-file manifest because the
|
||||||
before its first backend probe and holds it through repair, import/reset,
|
sender controls both today's RAR CRC32 metadata and extracted bytes. The
|
||||||
peer-runtime stop/restart, and application exit. The lease exists even while
|
receiver accepts exactly the expected path set, sizes, and SHA-256 values in
|
||||||
networking is stopped. File-backed state uses `<state_dir>/identity.lock`;
|
isolated staging, then applies the documented local account/language rewrite
|
||||||
explicit identity files use an adjacent lease. The fixed OS-account keyring
|
and promotes the transaction. CRC32 may remain as an early corruption check,
|
||||||
locator uses one canonical account-wide lease independent of caller state
|
but it is not the security boundary. A game without a verified extracted
|
||||||
directory. Default persistent keyring use is supported only through that
|
manifest does not offer Stream Install; there is no unverified fallback or
|
||||||
canonical application profile; every other/headless profile uses an explicit
|
warning-through button.
|
||||||
seed or identity file.
|
|
||||||
- Lock contention returns `IdentityBusy`; it never triggers generation.
|
|
||||||
- Creation/import/reset/backend migration validates input first. A file backend
|
|
||||||
writes a restrictive unique temporary file, flushes it, atomically renames
|
|
||||||
it over the selected record, and syncs the containing directory where
|
|
||||||
supported. A keyring backend uses the platform's atomic record replacement,
|
|
||||||
then reads it back and verifies the derived key and ID. Platforms that cannot
|
|
||||||
provide atomic record replacement do not enable in-place keyring
|
|
||||||
import/reset; they require explicit backend migration or backup/repair.
|
|
||||||
Initial creation and backend migration write and verify the new secret before
|
|
||||||
atomically writing the reconstructible sidecar; migration retires the old
|
|
||||||
backend only after the sidecar commits.
|
|
||||||
|
|
||||||
Backend migration additionally uses a small non-secret
|
Hashing is performed in the existing streaming I/O path. The acceptance gate
|
||||||
`identity.migration.json { version, from, to, target_fingerprint, stage }`
|
measures end-to-end throughput on the standard LAN workload and avoids a
|
||||||
intent, written before touching the target and cleared only after sidecar
|
second full read when complete chunk coverage already proves the file bytes.
|
||||||
switch plus old-backend cleanup. When present, startup ignores the ordinary
|
|
||||||
valid-sidecar fast path, probes only the named source/target, and idempotently
|
|
||||||
resumes or explicitly rolls back. Before target write, `to` must be `NoEntry`
|
|
||||||
or contain the same identity; a different identity is a repair conflict, never
|
|
||||||
overwritten. This closes both crash windows without a generation-numbered
|
|
||||||
two-resource commit protocol.
|
|
||||||
|
|
||||||
Every intent stage is durably updated by atomic rename. A corrupt or
|
### 3.3 Use a simple installation-local TLS identity
|
||||||
unsupported intent is a typed non-mutating repair state and is never ignored.
|
|
||||||
Resume/rollback deletes a backend record only after rereading it and matching
|
|
||||||
the intent fingerprint.
|
|
||||||
|
|
||||||
- A crash after secret write and before sidecar write is recoverable by the
|
The identity exists to bind a live peer and its changing address to TLS. It is
|
||||||
missing-sidecar table. A replacement crash can yield old-sidecar/new-secret
|
not exposed as a user credential.
|
||||||
mismatch; startup stops and offers explicit completion or backup restore.
|
|
||||||
No generation-numbered two-resource transaction is required.
|
|
||||||
- Same-backend keyring replacement may leave either old or new secret after a
|
|
||||||
crash; a sidecar mismatch is an explicit recoverable state, never a reason to
|
|
||||||
generate or fall back. The UI requires a verified encrypted backup before a
|
|
||||||
destructive in-place replacement. File replacement and backend migration do
|
|
||||||
not retire the old selected secret before the replacement is safely exposed.
|
|
||||||
- Phase 1b leaves the legacy UUID `peer_id` as the protocol-7 runtime/advertised
|
|
||||||
ID even after the cryptographic identity is durable. Phase 2's protocol bump
|
|
||||||
atomically activates the derived PeerId for networking and only then deletes
|
|
||||||
the UUID. It is never cryptographic continuity evidence.
|
|
||||||
|
|
||||||
The GUI uses Tauri `app_data_dir()`. `~/.lanspread` is only the core default
|
- Generate one self-issued TLS certificate/key pair in Tauri's
|
||||||
when no state directory is supplied. Docs provide an update/uninstall matrix
|
`app_data_dir()` and store it in one versioned application file with
|
||||||
per package rather than promising universal reinstall survival.
|
restrictive permissions where the platform supports them.
|
||||||
|
- Prefer Ed25519 if the selected s2n-quic rustls provider supports the complete
|
||||||
|
responder-verification path. Otherwise use one supported P-256 TLS key. Do not
|
||||||
|
add a second signing identity or a custom certificate-extension binding.
|
||||||
|
- Define `PeerId` as lowercase unpadded base32 of
|
||||||
|
`SHA-256(canonical DER SubjectPublicKeyInfo)` from the actual TLS key. The
|
||||||
|
same key is therefore both the identity and the TLS proof-of-possession key.
|
||||||
|
- Validate on load that the private key, certificate SPKI, and derived ID
|
||||||
|
agree. Never log private material.
|
||||||
|
- A valid file is reused. A missing or corrupt file is regenerated
|
||||||
|
automatically (quarantining corrupt bytes best-effort) and produces at most
|
||||||
|
a diagnostic log entry. If persistence is unavailable, use a fresh in-memory
|
||||||
|
identity for that run and show a non-blocking diagnostic; LAN functionality
|
||||||
|
should not become a repair wizard.
|
||||||
|
- The peer CLI may accept an explicit deterministic identity file/seed for
|
||||||
|
repeatable tests. It does not probe keyrings or share a default container
|
||||||
|
identity accidentally.
|
||||||
|
|
||||||
#### Repair, backup, import, and reset
|
There is no OS-keyring backend, sidecar/backend reconciliation, migration
|
||||||
|
intent, identity lease, encrypted backup, import, reset, clone warning, or
|
||||||
|
continuity repair UI. If application data is lost, the installation simply
|
||||||
|
appears as a new nearby peer. Because authorization is not attached to the old
|
||||||
|
ID, nothing security-sensitive needs migration.
|
||||||
|
|
||||||
Phase 1 ships a typed repair surface, not a log-only startup failure: retry and
|
### 3.4 Pin responders and make remote state pull-only
|
||||||
unlock guidance, import backup, choose an unambiguous discovered backend,
|
|
||||||
deliberate file-backed creation on a genuinely fresh system, and deliberate
|
|
||||||
reset. The shell may start, but networking remains stopped until repair
|
|
||||||
succeeds.
|
|
||||||
|
|
||||||
Export emits only a versioned authenticated passphrase-encrypted seed envelope.
|
Every outbound operation accepts a first-class endpoint:
|
||||||
Phase 0 freezes its magic/version, exact KDF and cost/salt encoding, exact AEAD
|
|
||||||
and nonce/tag encoding, authenticated header, and maximum decoded size before
|
|
||||||
Phase 1b implements it. Raw seed material never reaches frontend state, logs,
|
|
||||||
events, or errors. The UI calls this a **backup** and warns that importing it
|
|
||||||
creates two cryptographically indistinguishable devices. “Move” means verify
|
|
||||||
the destination fingerprint and deliberately retire/reset the source; stale
|
|
||||||
copies cannot be revoked without later key rotation. Reset shows old/new
|
|
||||||
fingerprints and explains that all remote continuity records are invalidated.
|
|
||||||
|
|
||||||
Import over an existing identity requires old/new fingerprint confirmation and
|
|
||||||
replaces the selected backend unless the user separately chooses backend
|
|
||||||
migration. Reset/import fails if that backend cannot be replaced or retired;
|
|
||||||
it never creates a fallback identity. Identity replacement first stops
|
|
||||||
networking, commits under the still-held application lease, rebuilds all
|
|
||||||
runtime identity/TLS state, and only then restarts networking.
|
|
||||||
|
|
||||||
### 7.3 Trust state and source permission
|
|
||||||
|
|
||||||
Trust is keyed by verified full `PeerId` and public key, never by display name
|
|
||||||
or address. A record contains at least:
|
|
||||||
|
|
||||||
```text
|
|
||||||
public_key
|
|
||||||
first_seen, last_seen
|
|
||||||
observed_names, pinned_name
|
|
||||||
acknowledged_at // null means persistently “new”
|
|
||||||
blocked // overrides every other policy
|
|
||||||
download_from // prompt | allowed | denied
|
|
||||||
rotated_from // later continuity feature
|
|
||||||
```
|
|
||||||
|
|
||||||
- First contact stores `download_from = prompt`, which is not eligible to
|
|
||||||
source a transfer. Only an explicit user action sets `allowed`.
|
|
||||||
- The UI wording is “Allow downloads from this device,” never “trust this
|
|
||||||
device/content.”
|
|
||||||
- A familiar name under a new key creates a separate new record and a conflict
|
|
||||||
warning. It never inherits acknowledgement, block, or source permission.
|
|
||||||
- Setting `blocked` atomically changes `download_from` to `denied`, suppresses
|
|
||||||
candidate retries, rejects direct authenticated protocol actions, and rejects
|
|
||||||
independently signed objects from that author even when relayed by an
|
|
||||||
allowed peer. Existing signed objects are quarantined inside the same bounded
|
|
||||||
CTP store, remain charged to the author/global quotas, are excluded from
|
|
||||||
reduction and relay, and preserve tombstone evidence. Unblocking leaves
|
|
||||||
source permission denied and triggers an authenticated resync before
|
|
||||||
quarantined state is reconsidered.
|
|
||||||
- Block, pin/acknowledgement, source permission, import, and reset are written
|
|
||||||
atomically and durably before success is reported. Only observational data
|
|
||||||
such as `last_seen` and name telemetry may be debounced.
|
|
||||||
- Final-state serving remains open to authenticated, nonblocked peers; party
|
|
||||||
admission can narrow that later. During Phases 2–3, inbound read-only
|
|
||||||
requesters are deliberately anonymous and serving remains public even to a
|
|
||||||
caller that might hold a blocked key. Direct requester blocking becomes
|
|
||||||
enforceable at the Phase 4 envelope boundary. This intermediate limitation
|
|
||||||
affects what others may pull from us, not local state mutation or which
|
|
||||||
devices we download from. Local/catalog-matching already-downloaded data
|
|
||||||
remains usable without any remote-source approval.
|
|
||||||
|
|
||||||
Hard trust/admission bounds prevent sequential Sybil growth: at most 1024
|
|
||||||
persistent trust records, eight observed names per record, and 256
|
|
||||||
unacknowledged relay/direct-new observations. A directly verified identity or
|
|
||||||
relay-only author does not create durable trust state when the relevant bound
|
|
||||||
is full; acknowledged, blocked, or explicitly source-authorized records are
|
|
||||||
never silently evicted. Relay-only authors remain bounded ephemeral metadata
|
|
||||||
unless the user acknowledges or blocks them or direct verified contact occurs.
|
|
||||||
The UI reports capacity and lets the user remove records deliberately.
|
|
||||||
|
|
||||||
### 7.4 First-class endpoints and discovery candidates
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
struct PeerEndpoint {
|
struct PeerEndpoint {
|
||||||
@@ -463,960 +274,315 @@ struct PeerEndpoint {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The endpoint is captured at discovery/source selection and carried through
|
This endpoint is carried through discovery handshake, library refresh,
|
||||||
handshake, library/manifest fetch, ordinary downloads, chunk plans, retries,
|
Call-to-Play refresh, metadata/content requests, chunk plans, retries, streamed
|
||||||
streamed install, healing, liveness, and direct peer-CLI operations.
|
install, healing, liveness, and direct peer-CLI operations. Delete
|
||||||
Every outbound connection requires it or equivalent mandatory `(expected_id,
|
address-derived IDs, unique-IP identity fallbacks, and address-only connects.
|
||||||
addr)` arguments.
|
|
||||||
|
|
||||||
Delete `remote_peer::ensure_peer_id_for_addr`, fabricated `addr-*` IDs,
|
mDNS supplies bounded candidates containing `(peer_id, addr, protocol,
|
||||||
unique-IP identity fallbacks, and address-only direct connect. The peer-CLI
|
revision hints)`. It may cause a dial, but it never directly creates or
|
||||||
`ConnectPeer` operation requires both full ID and address or is removed; there
|
updates authenticated peer/library/Call-to-Play state. A candidate becomes a
|
||||||
is no desktop TOFU bootstrap UI to design for that test-only command.
|
peer only after a successful outgoing TLS connection to its advertised address
|
||||||
|
proves the expected `PeerId`.
|
||||||
|
|
||||||
mDNS owns a bounded candidate directory separate from authenticated peer
|
Use TLS 1.3 with a self-issued certificate. The custom client verifier must:
|
||||||
records. Initial values are 256 candidates, 128 simultaneously verified live
|
|
||||||
peers, 120 new candidate insertions/minute globally, and 8/minute per
|
|
||||||
advertised target IP (the address exposed by the current mDNS wrapper, not an
|
|
||||||
authenticated packet source). Phase 0 confirms those values and candidate TTL/backoff against the honest
|
|
||||||
three-peer harness. At capacity, expired candidates are removed first; a new
|
|
||||||
candidate/live peer is otherwise rejected without evicting an authenticated
|
|
||||||
record. It may advertise `(peer_id, addr, public_key, revision hints)`, but a
|
|
||||||
key/ID consistency check does not prove possession or address control. mDNS
|
|
||||||
addition, expiry, or change never directly upserts, rebinds, removes, merges a
|
|
||||||
library, or refreshes authenticated liveness. A blocked ID is not retried.
|
|
||||||
|
|
||||||
Candidate retries are deduplicated with jittered backoff while the
|
1. parse only the selected certificate/SPKI shape;
|
||||||
advertisement remains live. Reconciliation cannot depend on mDNS emitting a
|
2. derive the `PeerId` from that SPKI and compare the full value with the
|
||||||
second identical `ServiceResolved`; a still-live candidate can re-establish a
|
endpoint's expected ID; and
|
||||||
peer after transient failure or liveness removal.
|
3. perform real TLS 1.3 CertificateVerify validation under the presented key.
|
||||||
|
|
||||||
One shared socket predicate applies to mDNS, Hello, and Ack candidates: port is
|
The load-bearing negative test presents peer A's certificate/SPKI with peer
|
||||||
nonzero; address is neither unspecified, multicast, nor IPv4 broadcast;
|
B's private key and requires the handshake to fail. Also reject a different
|
||||||
loopback is accepted only in explicit test mode; and IPv6 link-local addresses
|
valid peer at a reused address. Use one version-bound ALPN, disable 0-RTT, and
|
||||||
carry the observed interface scope. Global IPv6 and private/ULA addresses are
|
start without TLS session resumption so every short-lived connection performs
|
||||||
not rejected merely because they are not syntactically “private”; interface
|
the simple full proof.
|
||||||
provenance and the bounded dialing policy define LAN reachability.
|
|
||||||
|
|
||||||
### 7.5 Authenticated QUIC responder
|
The protocol is deliberately responder-authenticated rather than wrapping
|
||||||
|
every message in a signature:
|
||||||
|
|
||||||
Use the s2n-quic rustls provider with TLS 1.3 only.
|
- Requests that read public library/content state may be made by any LAN
|
||||||
|
client while Local network sharing is enabled.
|
||||||
|
- A response is authoritative only to the initiator that connected using the
|
||||||
|
expected `PeerEndpoint`; the TLS channel supplies integrity and request/
|
||||||
|
response correlation.
|
||||||
|
- A state-bearing response contains only the responder's own state. It cannot
|
||||||
|
vouch for third parties.
|
||||||
|
- Inbound `LibraryChanged` or `CallToPlayChanged` messages are untrusted hints.
|
||||||
|
For a known claimed ID they schedule one coalesced, rate-limited pull from
|
||||||
|
that ID's already known endpoint. Their payload never merges directly. Hints
|
||||||
|
for unknown IDs are ignored and mDNS remains the discovery path.
|
||||||
|
- `Hello` becomes a pull-oriented exchange: the initiator sends no
|
||||||
|
authoritative identity or replicated state, and the pinned responder returns
|
||||||
|
its own current snapshot.
|
||||||
|
|
||||||
Phase 0 spikes RFC 7250 raw public keys end to end through
|
This extra pull is one small LAN round trip and removes general signed
|
||||||
`s2n-quic-rustls`. If RPK works, use the identity public key directly. Otherwise
|
envelopes, canonical opaque payloads, nonce caches, replay semantics, inbound
|
||||||
use a self-issued Ed25519 X.509 leaf whose SPKI is exactly the identity key.
|
client-certificate plumbing, and connect-back authority state.
|
||||||
CA, CN, SAN, hostname, and self-signature are not authentication inputs; the
|
|
||||||
expected `PeerId` is.
|
|
||||||
|
|
||||||
The custom client verifier captures the expected `PeerId` from
|
An unproven address collision never evicts an authenticated peer. If a pinned
|
||||||
`PeerEndpoint` and MUST:
|
dial later proves that a different ID now owns the same address, atomically
|
||||||
|
replace address ownership and retire the old record only if it still names
|
||||||
|
that address/generation. A same-ID address move is likewise committed only
|
||||||
|
after pinning the new endpoint. Pings are also pinned, and a late ping result
|
||||||
|
may update/remove only the same endpoint generation it probed so it cannot
|
||||||
|
delete a peer that has already moved or reconnected.
|
||||||
|
|
||||||
1. require the selected RPK/SPKI shape and allowed signature scheme;
|
Remove `Goodbye`. It is unnecessary for correctness and an unauthenticated
|
||||||
2. extract the raw identity key;
|
removal hint is unsafe. mDNS expiry plus responder-pinned liveness handles
|
||||||
3. derive its full `PeerId` and compare it to the expected value; and
|
departure.
|
||||||
4. cryptographically verify TLS 1.3 CertificateVerify against that presented
|
|
||||||
key using rustls/webpki's verification helper.
|
|
||||||
|
|
||||||
It may return `HandshakeSignatureValid::assertion()` only after step 4
|
### 3.5 Keep Call to Play direct and ephemeral
|
||||||
succeeds. It must advertise only schemes it actually verifies. The load-bearing
|
|
||||||
negative test presents peer A's certificate/SPKI with peer B's private key and
|
|
||||||
requires the handshake to fail.
|
|
||||||
|
|
||||||
RPK mode requires `requires_raw_public_keys() == true`, a raw-key server
|
Call to Play is coordination among people currently at the party. It does not
|
||||||
resolver (`AlwaysResolvesServerRawPublicKeys` or equivalent), and
|
need a Byzantine replicated ledger.
|
||||||
`verify_tls13_signature_with_raw_key`. X.509 mode uses the certificate-specific
|
|
||||||
signature helper. The selected mode and mismatched-mode failures are tested.
|
|
||||||
|
|
||||||
Disable TLS resumption/session tickets and QUIC 0-RTT/early application data.
|
Each runtime owns only its locally authored slice:
|
||||||
Every connection performs current proof of possession; the in-memory nonce
|
|
||||||
cache is not durable replay protection. SNI remains
|
|
||||||
`<expected_peer_id>.lanspread`, but the verifier's captured full expected ID is
|
|
||||||
authoritative.
|
|
||||||
|
|
||||||
Client and server advertise exactly one version-bound ALPN:
|
|
||||||
`b"lanspread-peer/" || ASCII_decimal(PROTOCOL_VERSION)`. Any absent or different
|
|
||||||
ALPN fails the handshake. TLS 1.2 is disabled and
|
|
||||||
`verify_tls12_signature` always rejects. In RPK mode the transmitted key is the
|
|
||||||
canonical RFC 7250 Ed25519 DER SubjectPublicKeyInfo, while `PeerId` derivation
|
|
||||||
still hashes the extracted raw 32-byte key.
|
|
||||||
|
|
||||||
Keep P-256 TLS as a last fallback until the RPK/Ed25519 certificate spike
|
|
||||||
succeeds. A split P-256/Ed25519 design is allowed only if the verifier receives
|
|
||||||
and validates a required critical certificate extension before channel
|
|
||||||
acceptance. The Ed25519 signature input is exactly:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
b"lanspread/tls-key-binding/v1\0"
|
CallId { creator: PeerId, random_nonce }
|
||||||
|| u32_be(PROTOCOL_VERSION)
|
|
||||||
|| identity_key[32]
|
|
||||||
|| u32_be(tls_spki_der_length)
|
|
||||||
|| tls_spki_der[tls_spki_der_length]
|
|
||||||
```
|
|
||||||
|
|
||||||
The verifier derives and matches the expected identity, verifies this binding,
|
CallToPlayAuthorSnapshot {
|
||||||
then verifies CertificateVerify under the P-256 SPKI. A missing/tampered
|
runtime_session_id
|
||||||
extension or statement delivered later inside `Hello` is rejected. If this
|
revision
|
||||||
pre-channel binding is not practical, the fallback is forbidden.
|
display_name
|
||||||
|
events[] // actor ID is not a wire field
|
||||||
If X.509 is used, configure
|
|
||||||
`rcgen = { default-features = false, features = ["aws_lc_rs"] }` (plus `pem`
|
|
||||||
only if actually needed) so the plan does not silently add `ring` as a second
|
|
||||||
crypto stack.
|
|
||||||
|
|
||||||
### 7.6 Outbound binding, inbound Hello, and connect-back
|
|
||||||
|
|
||||||
A successful outgoing handshake records the endpoint actually dialled under
|
|
||||||
the pinned key. `HelloAck.listen_addr` and every other advertised address are
|
|
||||||
new candidates only; they never redirect the successful binding.
|
|
||||||
|
|
||||||
Phase 2 authenticates the responder, not an incoming client. Therefore an
|
|
||||||
inbound `Hello` is non-authoritative:
|
|
||||||
|
|
||||||
- protocol 8 `Hello` is candidate-only: current protocol, claimed ID/key,
|
|
||||||
listener, bounded revision/feature hints, and
|
|
||||||
`request_connect_back: bool`; it carries no library or CTP vector;
|
|
||||||
- perform only bounded cheap syntax/version checks;
|
|
||||||
- it may receive the responder-authenticated Ack and read-only public data
|
|
||||||
allowed by policy;
|
|
||||||
- do not upsert/remove/rebind a peer, merge its library or Call-to-Play state,
|
|
||||||
create trust continuity, or refresh listener reachability; and
|
|
||||||
- enqueue at most one bounded, deduplicated connect-back to the claimed
|
|
||||||
`PeerEndpoint` when `request_connect_back` is true.
|
|
||||||
|
|
||||||
Discovery's first probe sets `request_connect_back = true`. A connect-back or
|
|
||||||
ordinary pinned resync sets it to false and is terminal: its responder returns
|
|
||||||
the state-bearing Ack but never schedules another callback. Because inbound
|
|
||||||
Hello is non-authoritative in both modes, an attacker setting false gains no
|
|
||||||
state-mutation shortcut. Each side learns the other only from its own outbound
|
|
||||||
pinned connection, so the initial probe plus one callback establishes both
|
|
||||||
directions without recursion.
|
|
||||||
|
|
||||||
The connect-back requires the claimed ID as the TLS-pinned responder. Only its
|
|
||||||
authenticated response may merge state and bind the endpoint that was actually
|
|
||||||
dialled. A claimed target must be a nonzero unicast LAN socket whose IP equals
|
|
||||||
the inbound QUIC source IP. mDNS candidates use their separate bounded dialing
|
|
||||||
path; a forged advertisement is not callback authorization. Permit at most one
|
|
||||||
in-flight callback per endpoint, two per source IP, and 16 globally, with the
|
|
||||||
candidate admission rates above, deduplication, expiry, and jittered backoff.
|
|
||||||
These controls bound rather than eliminate LAN scan/reflection and
|
|
||||||
work-amplification risk. Hello, mDNS, and legacy library hints all feed one
|
|
||||||
coalescing scheduler with these per-source/advertised-target, per-PeerId, and
|
|
||||||
global budgets; blocked IDs are dropped before scheduling. A legacy library
|
|
||||||
hint can target only an already verified recorded `PeerEndpoint`; its claimed
|
|
||||||
address and delta body are ignored, and an unknown claimed ID is dropped.
|
|
||||||
|
|
||||||
Before independently signed events land, no client-originated state-changing
|
|
||||||
object is authoritative. A legacy library notification must trigger the
|
|
||||||
bounded pinned resync so post-start honest library updates still propagate;
|
|
||||||
legacy CTP networking is disabled for protocol 8. During
|
|
||||||
Phase 3, only independently verified signed Call-to-Play objects may mutate
|
|
||||||
their own store through an otherwise unauthenticated outer request; outer
|
|
||||||
sender/address claims remain non-authoritative. All other client-originated
|
|
||||||
state changes wait for Phase 4 envelopes.
|
|
||||||
|
|
||||||
Peer binding is a typed atomic operation. If an address is already bound to a
|
|
||||||
different ID, reject the candidate and preserve both maps and the established
|
|
||||||
peer. A same-ID move is accepted only after pinning the new endpoint and
|
|
||||||
establishing that the old record is no longer current/reachable, then both
|
|
||||||
indexes change atomically. If old and new endpoints simultaneously prove
|
|
||||||
possession of the same key, preserve the established binding, quarantine the
|
|
||||||
candidate, and surface a duplicate-identity/clone conflict instead of flapping
|
|
||||||
between them. An unproven collision never evicts anyone.
|
|
||||||
|
|
||||||
### 7.7 Liveness and removal
|
|
||||||
|
|
||||||
Remove `Goodbye` from the protocol, handlers, shutdown flow, threat claims,
|
|
||||||
and tests. Authenticated liveness expiry is the sole remote-removal mechanism.
|
|
||||||
|
|
||||||
Liveness pings take `PeerEndpoint` and pin the responder. Successful outbound
|
|
||||||
authenticated contact proves endpoint reachability. An inbound signed frame
|
|
||||||
may prove identity activity after envelopes land, but it does not prove that
|
|
||||||
the advertised listener is reachable and does not refresh that endpoint's
|
|
||||||
reachability timeout.
|
|
||||||
|
|
||||||
Every liveness task snapshots the peer-record revision together with the ID,
|
|
||||||
address, and reachability timestamp. The revision increments on every
|
|
||||||
successful endpoint-reachability refresh and every rebind, including activity
|
|
||||||
while an older probe is pending. A late success or failure may update or remove
|
|
||||||
only if the current record still has that revision, endpoint, and observed
|
|
||||||
reachability timestamp.
|
|
||||||
`remove_peer_if_current` or equivalent compare-and-remove logic prevents a
|
|
||||||
stale probe from deleting a rebound/reconnected peer or cancelling its active
|
|
||||||
downloads.
|
|
||||||
|
|
||||||
The same conditional comparison applies to asynchronous ping failures and the
|
|
||||||
periodic stale-prune sweep. Phase 2 removes pre-dispatch
|
|
||||||
`note_peer_activity`/address-based attribution entirely.
|
|
||||||
|
|
||||||
### 7.8 Exact signed control envelopes
|
|
||||||
|
|
||||||
Every control `Request` and `Response` uses:
|
|
||||||
|
|
||||||
```text
|
|
||||||
SignedEnvelope {
|
|
||||||
protocol_version: u32,
|
|
||||||
sender: PeerId,
|
|
||||||
sender_key: PublicKey,
|
|
||||||
recipient: PeerId,
|
|
||||||
nonce: Nonce,
|
|
||||||
context: request | response,
|
|
||||||
payload: opaque bytes,
|
|
||||||
signature: Signature,
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
All fields are required; unknown fields are rejected. The inner request or
|
The local core creates call/event IDs, increments the revision after each
|
||||||
response is serialized once with `serde_json::to_vec`, stored as explicit
|
accepted local action, and sends a cheap change hint to known peers. A receiver
|
||||||
base64url payload bytes, signed, and verified before deserialization. The outer
|
coalesces the hint, connects to the author's known `PeerEndpoint`, and pulls
|
||||||
JSON is not canonical and is not itself signed.
|
that author's complete current slice. Because the responder is pinned, the
|
||||||
|
receiver assigns the author ID itself. A peer cannot put another actor ID into
|
||||||
|
the wire object.
|
||||||
|
|
||||||
The exact signature input is:
|
Snapshots use replacement, not union/CRDT semantics. For each peer, permit one
|
||||||
|
in-flight refresh; a newer revision for the same runtime session replaces that
|
||||||
|
author's previous slice atomically. A new runtime session replaces the old
|
||||||
|
session after a fresh pinned handshake. Stale concurrent results cannot
|
||||||
|
overwrite the current session.
|
||||||
|
|
||||||
```text
|
Authority rules remain simple:
|
||||||
b"lanspread/control-envelope/v1\0"
|
|
||||||
|| u32_be(protocol_version)
|
|
||||||
|| context_tag // request = 0x01, response = 0x02
|
|
||||||
|| sender_ascii[52]
|
|
||||||
|| sender_key[32]
|
|
||||||
|| recipient_ascii[52]
|
|
||||||
|| nonce[16]
|
|
||||||
|| u32_be(payload_length)
|
|
||||||
|| payload[payload_length]
|
|
||||||
```
|
|
||||||
|
|
||||||
Ed25519 signs these bytes directly. Receivers require the current
|
- `Create`, `Start`, `Cancel`, and `AddTime` are effective only when the pinned
|
||||||
`PROTOCOL_VERSION`, exact fixed lengths, expected context/direction,
|
author equals `CallId.creator`.
|
||||||
`recipient == local_peer_id`, `derive_peer_id(sender_key) == sender`, and a
|
- RSVP, ready/leave, and chat actions are attributed to the pinned author. They
|
||||||
valid signature before parsing the payload or dispatching a handler. A client
|
become effective only while the referenced creator root is directly
|
||||||
also requires `Response.sender` to equal the TLS-pinned responder. A response
|
present; an author slice pulled before its creator is retained within its
|
||||||
sets its recipient to the verified request sender, copies the verified request
|
ordinary bound but remains hidden until that creator's direct pull arrives.
|
||||||
nonce, and the initiator requires equality, binding the pair.
|
- A snapshot contains only events authored by its responder. Third-party
|
||||||
|
events are rejected rather than relayed.
|
||||||
|
- Display names never grant authority.
|
||||||
|
|
||||||
Boundary verification yields `VerifiedSender`; handlers no longer accept or
|
A newly arriving peer discovers and pulls directly from every live peer, so it
|
||||||
trust payload `peer_id` fields. Control envelopes are hop-by-hop and are never
|
reconstructs calls from the people still present. If an author's peer goes
|
||||||
forwarded.
|
away, remove that author's slice. If the creator goes away, the call disappears
|
||||||
|
from the derived view. A participant who leaves naturally drops out. This is
|
||||||
|
the intended session model, not data loss.
|
||||||
|
|
||||||
The nonce table is per sender/context, bounded, checked-and-inserted atomically
|
Keep the useful human-scale timers: active calls expire, unresolved expired
|
||||||
only after signature verification, and in memory only. Cache loss or eviction
|
calls may remain visible for five minutes, and Start/Cancel results may remain
|
||||||
can cause a duplicate to be processed, so state-changing handlers remain
|
visible for fifteen minutes. After that, the author drops them from its current
|
||||||
idempotent or revision/event-ID guarded. There is no wall-clock signature
|
snapshot. There are no session-long tombstones, rootless terminal records,
|
||||||
expiry and no claim of durable replay prevention.
|
three-day history horizons, verification caches, or permanent anti-resurrection
|
||||||
|
state because no third party can replay an old author's history as authority.
|
||||||
|
|
||||||
Every request initiator generates a fresh 16-byte CSPRNG nonce and never
|
Retain straightforward schema and resource limits: bounded strings/chat,
|
||||||
intentionally reuses it. The duplicate cache is capped at 64 entries per
|
bounded events and encoded bytes per author, bounded total live peers, and a
|
||||||
sender/context, 256 represented senders, and 8192 entries globally; admission
|
named control-frame maximum. Validate one author's snapshot off to the side and
|
||||||
of a new sender/cache entry is rejected when the applicable bound cannot be
|
accept or reject it as a unit; a bad/oversized peer cannot consume another
|
||||||
met after normal LRU expiry. A response copies rather than generates the
|
author's slice or the local author's capacity. Exact limits are set from the
|
||||||
request nonce.
|
existing three-peer and stress fixtures, not from an internet-scale adversary
|
||||||
|
model.
|
||||||
|
|
||||||
Bulk responder-to-initiator chunk and stream-install frames remain unsigned
|
A malicious creator can show inconsistent versions of its own noncritical call
|
||||||
inside the responder-pinned TLS channel. That deliberate asymmetry preserves
|
to different peers. This plan accepts that limit rather than adding signatures,
|
||||||
throughput. Every retry still uses the selected `PeerEndpoint`; TLS must never
|
gossip, consensus, or permanent storage to a party invitation feature.
|
||||||
fall back to “whoever is at this address.”
|
|
||||||
|
|
||||||
The length-delimited control codec gets a named maximum below its current 8
|
### 3.6 Keep the UI about games and people
|
||||||
MiB default after Phase 0 measures the largest bounded library/manifest
|
|
||||||
message. Any bounded payload that cannot fit must be paginated rather than
|
|
||||||
raising the frame indefinitely. The encoded outer-frame cap is enforced before
|
|
||||||
frame allocation; a bounded base64 visitor checks computed decoded length
|
|
||||||
before allocating the decoded payload. Both happen before signature work and
|
|
||||||
inner deserialization.
|
|
||||||
|
|
||||||
### 7.9 Independently signed Call-to-Play events
|
Add one visible `Local network sharing` setting, on by default for this
|
||||||
|
LAN-sharing application. When off, stop mDNS advertisement/discovery, the QUIC
|
||||||
|
listener, outbound refresh, and serving. The setting is durable and its state
|
||||||
|
is obvious in the main UI/settings.
|
||||||
|
|
||||||
Call-to-Play objects land before general signed envelopes so relayed authorship
|
Do not add per-peer source prompts. Every peer with the locally expected
|
||||||
is independently safe as early as possible.
|
`content_id` is an eligible swarm source; verification is automatic.
|
||||||
|
|
||||||
```text
|
Normal UI uses display names and peer count. A short PeerId suffix may
|
||||||
SignedCallToPlayEvent {
|
disambiguate duplicate names or appear in diagnostics, but there are no New,
|
||||||
protocol_version: u32,
|
Trusted, key-changed, backup, repair, or fingerprint-confirmation workflows.
|
||||||
event_id: EventId,
|
|
||||||
author: PeerId,
|
|
||||||
author_key: PublicKey,
|
|
||||||
body: opaque CallToPlayEventBody bytes,
|
|
||||||
signature: Signature,
|
|
||||||
}
|
|
||||||
|
|
||||||
CallToPlayEventBody {
|
User-facing exceptional states are concrete:
|
||||||
call_ref: CallRef { creator_key: PublicKey, create_nonce: CallNonce },
|
|
||||||
actor_name,
|
|
||||||
at,
|
|
||||||
action,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The body excludes `actor_id`, `call_id`, `event_id`, and independent chat
|
- `Verifying game files` while existing or newly received content is checked;
|
||||||
`message_id`. The author is derived from `author_key`; the redundant outer
|
- `A source sent invalid data; retrying another nearby peer` when recovery is
|
||||||
`author` must match. Core publication creates the 32-byte CSPRNG nonce for
|
in progress;
|
||||||
`Create`, resolves the retained `CallRef` for later actions, chooses identity
|
- `No nearby peer could provide the verified catalog version` after all
|
||||||
and IDs, and signs. The frontend never supplies those authority fields.
|
matching sources fail;
|
||||||
|
- `Nearby devices are running a different Lanspread version` when mDNS sees an
|
||||||
|
incompatible protocol; and
|
||||||
|
- a non-blocking networking diagnostic if the installation identity cannot be
|
||||||
|
persisted and will change next launch.
|
||||||
|
|
||||||
The exact event-signature transcript is:
|
Do not ask the user to solve a cryptographic implementation problem.
|
||||||
|
|
||||||
```text
|
## 4. One protocol cutover
|
||||||
b"lanspread/call-to-play-event/v1\0"
|
|
||||||
|| u32_be(protocol_version)
|
|
||||||
|| author_ascii[52]
|
|
||||||
|| author_key[32]
|
|
||||||
|| u32_be(body_length)
|
|
||||||
|| body[body_length]
|
|
||||||
```
|
|
||||||
|
|
||||||
The body encoding is UTF-8 RFC 8259 JSON with the exact field/action names in
|
Develop the pieces behind internal APIs, then replace protocol 7 with one new
|
||||||
the versioned proto schema, integer timestamps only, explicit URL-safe
|
current protocol (protocol 8 if the version has not moved). Do not ship
|
||||||
unpadded-base64 adapters for `PublicKey` and the 32-byte `CallNonce`, and no
|
intermediate protocol 8/9/10 designs and do not add compatibility decoding.
|
||||||
unknown or duplicate fields or trailing non-whitespace data. The local emitter
|
|
||||||
uses compact `serde_json` with declared struct-field order. Verification and
|
|
||||||
forwarding always use the exact received bytes and never reconstruct them; the
|
|
||||||
store retains body and signature verbatim. Fixed vectors freeze both the local
|
|
||||||
encoding and exact-byte verification behavior.
|
|
||||||
|
|
||||||
Derived IDs use full SHA-256 digests encoded as 52-character lowercase base32:
|
The cutover includes:
|
||||||
|
|
||||||
```text
|
- `PeerId` derived from the TLS SPKI and `PeerEndpoint` required by every
|
||||||
call_id = H("lanspread/ctp-call/v1\0" || creator_key || create_nonce)
|
outbound connection;
|
||||||
event_id = H("lanspread/ctp-event-id/v1\0" || u32_be(protocol_version)
|
- version-bound ALPN and per-installation server certificates instead of the
|
||||||
|| author_key || u32_be(body_length) || body)
|
repository-wide `cert.pem`/`key.pem`;
|
||||||
```
|
- mDNS candidate-only semantics and useful incompatible-version telemetry;
|
||||||
|
- responder-owned pull snapshots plus bounded change hints instead of inbound
|
||||||
|
state-bearing `Hello`, pushed `LibraryDelta`, and pushed/relayed
|
||||||
|
`CallToPlayEvents`;
|
||||||
|
- cryptographic `content_id` in game availability and catalog-driven chunk
|
||||||
|
requests;
|
||||||
|
- canonical forward-slash catalog paths;
|
||||||
|
- author-owned Call-to-Play snapshots; and
|
||||||
|
- removal of `Goodbye` and payload fields that pretend to identify an
|
||||||
|
authoritative sender.
|
||||||
|
|
||||||
The receiver first requires `event.protocol_version == PROTOCOL_VERSION`, then
|
Peers on another protocol remain excluded, as required by project policy. To
|
||||||
recomputes both IDs and verifies the signature before parsing/admission. The
|
reduce real LAN-party friction, make this one coordinated bump and surface the
|
||||||
event signature covers the transcript above. The content-derived event ID makes adversarial
|
version mismatch rather than failing silently.
|
||||||
same-ID/different-body conflicts unreachable; the enclosing `event_id` is also
|
|
||||||
the chat/UI message identity.
|
|
||||||
|
|
||||||
Authority rules:
|
## 5. Code ownership
|
||||||
|
|
||||||
- `Create`, `Start`, `Cancel`, and `AddTime` require
|
Keep the change inside existing crates unless implementation pressure proves a
|
||||||
`author_key == call_ref.creator_key`.
|
real reusable boundary; a new identity crate is not required by the design.
|
||||||
- `Respond`/RSVP, `Leave`, and `SendMessage` may be signed by a participant but
|
|
||||||
require a retained or same-batch valid `Create` for the same `CallRef`.
|
|
||||||
- A rooted nonterminal action, including `AddTime`, has reducer effect only
|
|
||||||
when it sorts strictly after canonical `Create` by `(at, event_id)`. A
|
|
||||||
correctly signed candidate with some valid root but currently before the
|
|
||||||
canonical Create remains retained/inactive within quotas so a later earlier
|
|
||||||
canonical Create can trigger deterministic recomputation. Only a
|
|
||||||
creator-signed `Start`/`Cancel` has root-independent burn semantics.
|
|
||||||
- A rootless `AddTime` or participant event is `NeedHistory`, not
|
|
||||||
independently applicable.
|
|
||||||
- A valid creator-signed rootless `Start` or `Cancel` is an independently
|
|
||||||
verifiable irrevocable burn certificate for that `CallRef`.
|
|
||||||
- Names remain signed self-assertions and never grant authority.
|
|
||||||
|
|
||||||
For a creator that equivocates, canonical `Create` is the minimum
|
|
||||||
`(at, event_id)`. Among all rooted or rootless `Start`/`Cancel` certificates,
|
|
||||||
the canonical terminal is also the minimum `(at, event_id)`. Rootless state
|
|
||||||
retains that exact winning terminal and order key, not only a set of call IDs.
|
|
||||||
An earlier valid terminal arriving later atomically replaces a later tombstone;
|
|
||||||
losers and all later nonterminal/root data are obsolete. This selection is
|
|
||||||
arrival- and grouping-independent. A stale `Create` can never resurrect a
|
|
||||||
burned `CallRef`.
|
|
||||||
|
|
||||||
Compaction retains the current rooted display history for 15 minutes, then the
|
|
||||||
single canonical terminal. Fresh peers accept and relay that self-proving
|
|
||||||
tombstone. This deliberately changes and repairs the current replication
|
|
||||||
contract. A rootless tombstone is internal anti-resurrection state and is not
|
|
||||||
rendered as a nomination because it lacks the root's game/deadline data. Later
|
|
||||||
valid rooted history may hydrate terminal display but never reopen the call.
|
|
||||||
|
|
||||||
The verification cache is a bounded LRU keyed by a digest of the exact signed
|
|
||||||
object, including context, author key, body, and signature. A hit may reuse only
|
|
||||||
the canonical object already verified; it must never bless an incoming
|
|
||||||
alternate signature merely because an event ID matches. Eviction changes
|
|
||||||
performance only.
|
|
||||||
|
|
||||||
Do not reject relayed events because `now - at` is large. Every timestamp,
|
|
||||||
deadline, and `ready_at` is an integer in
|
|
||||||
`1..=8_640_000_000_000_000` milliseconds (the positive ECMAScript Date range,
|
|
||||||
also exactly representable as an integer in JavaScript), and all arithmetic is
|
|
||||||
checked for overflow. Replicated validation uses only these deterministic
|
|
||||||
signed-field rules:
|
|
||||||
|
|
||||||
- every event `at` is positive;
|
|
||||||
- `Create.deadline > Create.at` and
|
|
||||||
`Create.deadline - Create.at <= 3 days`;
|
|
||||||
- `Create.scheduled_for` is absent or exactly equals `Create.deadline`;
|
|
||||||
- `AddTime.deadline` is strictly greater than the event's `at`; it has reducer
|
|
||||||
effect only when it also exceeds the effective deadline immediately before
|
|
||||||
it in the canonical fold; and
|
|
||||||
`AddTime.deadline - canonical_Create.at <= 3 days`; and
|
|
||||||
- `Respond.ready_at` is absent or lies in the inclusive interval
|
|
||||||
`[Respond.at, canonical_Create.at + 3 days]`.
|
|
||||||
|
|
||||||
Local direct publication may apply a one-sided future-skew check before
|
|
||||||
signing; receivers do not make signature validity depend on their current
|
|
||||||
clock.
|
|
||||||
|
|
||||||
To compute extensions, sort all otherwise valid `AddTime` events after the
|
|
||||||
canonical Create by bytewise-ASCII `(at, event_id)`, fold from
|
|
||||||
`Create.deadline`, and apply only strict deadline increases satisfying the
|
|
||||||
rules above. Retain structurally valid rooted candidates within quotas even
|
|
||||||
when currently ineffective. Rebuild that fold and every dependent-action
|
|
||||||
classification over the retained union whenever an earlier Create/extension
|
|
||||||
arrives. Missing-root objects remain `NeedHistory`; normal expiry or canonical
|
|
||||||
terminal compaction eventually removes inactive candidates. Backend and
|
|
||||||
frontend use the same numeric
|
|
||||||
timestamp and bytewise lowercase-base32 ID comparator; the frontend must not
|
|
||||||
use locale-dependent collation.
|
|
||||||
|
|
||||||
#### Call-to-Play limits and merge semantics
|
|
||||||
|
|
||||||
Initial hard limits, adjusted only through a measured Phase 0 decision:
|
|
||||||
|
|
||||||
| Limit | Value |
|
|
||||||
|---|---:|
|
|
||||||
| Exact encoded signed event | 4 KiB |
|
|
||||||
| Total stored events | 4096 |
|
|
||||||
| Total exact encoded stored bytes | 4 MiB |
|
|
||||||
| Per author, including local | 256 events and 256 KiB |
|
|
||||||
| Remote-author identities represented | 256 |
|
|
||||||
| Reserved inside global limits for local author | 256 events and 256 KiB |
|
|
||||||
| Verification cache | 4096 exact-object entries |
|
|
||||||
| Incoming CTP vector before signature work | 4096 events and 4 MiB |
|
|
||||||
|
|
||||||
All count and byte limits are conjunctive and include active events, rooted
|
|
||||||
terminal history, rootless tombstones, and quarantined blocked objects. Quotas
|
|
||||||
are charged to the signed author, not the transport relay; a locally authored
|
|
||||||
object relayed back still counts as local. Tombstones are never evicted for any
|
|
||||||
admission.
|
|
||||||
|
|
||||||
The remote pool stops at 3840 events and 4 MiB minus 256 KiB, preserving the
|
|
||||||
local reserve; the local author still has the same 256-event/256-KiB author cap.
|
|
||||||
The 256 represented-remote-author limit counts identities with retained remote
|
|
||||||
objects, not an ever-seen set. A creator terminal
|
|
||||||
may settle an already-retained rooted call at capacity if post-reduction
|
|
||||||
compaction brings usage within hard limits; it may also replace an existing
|
|
||||||
tombstone with the canonical earlier winner only when the replacement also
|
|
||||||
fits the byte cap. A new rootless terminal consumes normal global/per-author
|
|
||||||
capacity—Create+Cancel spam cannot bypass quotas. Under overload, rooted
|
|
||||||
display retention may compact immediately to the canonical terminal to
|
|
||||||
preserve the state-reducing action and hard bound.
|
|
||||||
|
|
||||||
Merge processing is:
|
|
||||||
|
|
||||||
1. enforce frame, vector, object-size, and structural bounds before signatures;
|
|
||||||
2. independently authenticate and schema/block-classify each object;
|
|
||||||
3. compute canonical/state-reducing replacements and a dependency-closed
|
|
||||||
candidate set, allowing a same-batch `Create`
|
|
||||||
regardless of vector order and the explicit rootless-terminal exception;
|
|
||||||
4. reduce and pressure-compact on a clone, then apply per-author and represented
|
|
||||||
author limits to capacity-increasing groups without letting one author
|
|
||||||
poison another;
|
|
||||||
5. after every author/represented-author/capacity filter, recompute dependency
|
|
||||||
closure to a fixed point (or admit a whole dependency component), so a
|
|
||||||
rejected Create cannot leave an admitted dependent participant action;
|
|
||||||
6. commit state reducers first. Admit the remaining capacity-increasing groups
|
|
||||||
only if they collectively fit global count/byte/remote-pool limits;
|
|
||||||
otherwise admit none of those groups (never an arrival-order prefix); and
|
|
||||||
7. canonicalize and swap the store atomically once.
|
|
||||||
|
|
||||||
Atomic commit remains a feature; before the explicit collective-global-
|
|
||||||
exhaustion condition, one hostile event does not reject every unrelated
|
|
||||||
author. After Sybil exhaustion, emit a persistent typed warning that
|
|
||||||
history is full and updates may be incomplete. Do not accuse the transport
|
|
||||||
relay. Recovery is block/quarantine followed by deliberate party-state reset or
|
|
||||||
restart and authenticated resync. Deterministic over-quota quarantine is later
|
|
||||||
hardening.
|
|
||||||
|
|
||||||
Responder-authenticated transport never confers third-party authorship.
|
|
||||||
Protocol 8 takes the simplest safe option: it disables all network
|
|
||||||
Call-to-Play ingestion/synchronization while leaving local UI state available.
|
|
||||||
Phase 3 restores network propagation with signed objects. A paginated Phase 3
|
|
||||||
snapshot uses a random snapshot ID, page index/count, declared total
|
|
||||||
event/decoded-byte counts, and full-snapshot digest; pages are assembled only
|
|
||||||
within one pinned handshake/session under the 4096-event/4-MiB cap and merged
|
|
||||||
once. Missing, duplicate, mixed-ID, digest-mismatched, expired, or over-limit
|
|
||||||
assemblies are discarded. Independently signed event notifications may be
|
|
||||||
merged directly. Neither an inner event author nor an unauthenticated outer
|
|
||||||
relay refreshes listener liveness; after Phase 4 only the verified envelope
|
|
||||||
sender may record identity activity, never endpoint reachability.
|
|
||||||
|
|
||||||
### 7.10 Download-source authorization
|
|
||||||
|
|
||||||
Browse/library summaries from nonblocked `prompt` or `denied` peers may remain
|
|
||||||
visible. They are not transfer authority.
|
|
||||||
|
|
||||||
At transfer start, the peer core—not Tauri—builds the authoritative manifest
|
|
||||||
from direct responses received over responder-pinned connections from
|
|
||||||
`download_from = allowed` identities. Every accepted tuple
|
|
||||||
`(game_id, canonical path, is_dir, size)` has exact attestation from at least
|
|
||||||
one allowed identity. Transfer consensus excludes unapproved peers; an
|
|
||||||
unapproved vote cannot introduce or select a descriptor that causes filesystem
|
|
||||||
mutation.
|
|
||||||
|
|
||||||
For ordinary downloads, the validated manifest maps each exact descriptor to
|
|
||||||
the allowed `PeerEndpoint`s that attested it. The plan, progress state, and
|
|
||||||
retry paths preserve that provenance. A retry chooses only an allowed identity
|
|
||||||
that advertised that exact descriptor. It never falls back from a file-specific
|
|
||||||
source set to a global address list. An address may refresh only by resolving
|
|
||||||
and pinning the same selected `PeerId`.
|
|
||||||
|
|
||||||
Streamed install has a distinct boundary: ordinary metadata describes root
|
|
||||||
archives, while `FileBegin` dynamically introduces extracted paths. A streamed
|
|
||||||
source is eligible only if it is allowed, TLS-pinned, and attested the complete
|
|
||||||
selected root-archive manifest—not merely one consensus file. Its extracted
|
|
||||||
frames stay inside isolated staging and are checked with §6's canonical,
|
|
||||||
reserved-path, symlink/reparse, count, and byte rules before each staged create.
|
|
||||||
There is no per-extracted-file fallback. Retrying the stream restarts it from an
|
|
||||||
allowed identity that attested the same complete archive manifest. A future
|
|
||||||
pre-attested extracted-manifest preamble would be a separate wire change, not
|
|
||||||
an assumption in this phase. No staged path is promoted until the terminal
|
|
||||||
`Complete` frame validates the observed entry count, aggregate bytes, archive
|
|
||||||
set, and stream result; failure discards the entire staging transaction.
|
|
||||||
|
|
||||||
Validate the complete authoritative/selected manifest using §6 before
|
|
||||||
`begin_version_ini_transaction` or any preparation. Then and only then may
|
|
||||||
storage mutate the requested game root.
|
|
||||||
|
|
||||||
Changing `allowed` to `denied`, or blocking a peer, durably changes policy
|
|
||||||
before success is reported and cancels the entire normal or streamed transfer
|
|
||||||
if it uses that source. Remove it from retries, restore/leave the installation
|
|
||||||
sentinel in the existing incomplete state, invoke the ordinary-download
|
|
||||||
discard/cleanup path for partial peer-owned payload, and cleanly discard
|
|
||||||
streamed staging; a new user-initiated attempt may replan from remaining
|
|
||||||
eligible sources. User-owned `local/` remains untouched.
|
|
||||||
|
|
||||||
This boundary stops an unapproved peer from supplying transfer metadata or
|
|
||||||
bytes. It does not verify bytes from an approved peer.
|
|
||||||
|
|
||||||
## 8. Wire and protocol evolution
|
|
||||||
|
|
||||||
- Transport identity and endpoint semantics: protocol 8.
|
|
||||||
- Independently signed Call-to-Play objects: protocol 9.
|
|
||||||
- Signed control envelopes: protocol 10.
|
|
||||||
- Optional party admission/rotation: a later bump only if implemented.
|
|
||||||
|
|
||||||
Exact numeric versions are rebased if the repository's current version changes
|
|
||||||
before implementation; the ordering and no-compatibility policy are fixed.
|
|
||||||
|
|
||||||
`Request`/`Response` become inner payloads of `SignedEnvelope`; identity fields
|
|
||||||
that duplicate the verified sender are removed. `Goodbye` is deleted. Protocol
|
|
||||||
8/9 transitional `Hello` carries a claimed identity only to request the pinned
|
|
||||||
connect-back. Transitional `HelloAck.peer_id/public_key` MUST exactly equal the
|
|
||||||
TLS-pinned identity or the response is rejected; binding and library merge are
|
|
||||||
always keyed from the pin, never those redundant fields.
|
|
||||||
Protocol 10 removes those duplicate inner sender ID/key fields and uses the
|
|
||||||
verified envelope sender (plus the TLS pin for responses) as the sole identity
|
|
||||||
authority. Advertised listener addresses remain candidates requiring proof.
|
|
||||||
Call-to-Play snapshots carry `SignedCallToPlayEvent` and use the bounded atomic
|
|
||||||
snapshot assembly in §7.9.
|
|
||||||
|
|
||||||
Protocol 9's `CallToPlayEvents` request contains only signed event objects or
|
|
||||||
snapshot-page data; it removes the legacy relay/sender `peer_id` field. The
|
|
||||||
outer transport origin grants no event authority. Remaining per-request sender
|
|
||||||
fields disappear with all control envelopes in protocol 10.
|
|
||||||
|
|
||||||
Protocol 8 standardizes newly produced manifest paths to `/`-separated
|
|
||||||
canonical form. Because that is inside the version bump, protocol-8 receivers
|
|
||||||
need no legacy producer fallback; the standalone protocol-7 validator's
|
|
||||||
one-time normalization exists only until the cutover.
|
|
||||||
|
|
||||||
| Version | Inbound control dispatch |
|
|
||||||
|---|---|
|
|
||||||
| 8 | `Hello` is candidate-only; Ping/browse/metadata/transfer reads are anonymous/public; LibraryDelta is a coalesced pinned-resync hint; network CTP is disabled; no inbound activity attribution; no `Goodbye`. |
|
|
||||||
| 9 | Version 8 behavior, except independently signed CTP objects/snapshot assemblies may mutate only the CTP store after per-object verification. |
|
|
||||||
| 10 | Every control request/response requires a verified envelope; block policy runs before inner dispatch. Bulk chunk/stream frames remain unsigned only inside the responder-pinned TLS connection created by that verified request. |
|
|
||||||
|
|
||||||
## 9. Code ownership map
|
|
||||||
|
|
||||||
New `crates/lanspread-identity`:
|
|
||||||
|
|
||||||
| Area | Responsibility |
|
| Area | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `key` | Ed25519 secret/public key, full PeerId derivation, zeroizing secret record |
|
| `lanspread-db` / `lanspread-compat` | Catalog content-manifest types and loading beside `game.db`. |
|
||||||
| `store` | Explicit modes, normalized backend results, transition table, lifetime lease, keyring/file storage, sidecar, repair API |
|
| `lanspread-proto` | `PeerId`, `PeerEndpoint`, `content_id`, pull snapshots, change hints, author-owned Call-to-Play wire types, and the one protocol version. No crypto or storage logic. |
|
||||||
| `sign` | Exact transcript signing and verification, verified sender/event construction |
|
| `lanspread-peer::identity` | Simple key/certificate load-or-generate, SPKI-derived ID, and test identity injection. |
|
||||||
| `tls` | RPK/X.509 material and pinned rustls client/server configuration |
|
| `lanspread-peer::network` | Per-endpoint rustls client config, full responder verification, ALPN, and no address-only connect. |
|
||||||
|
| discovery/handshake/liveness | Candidate-only mDNS, pinned pulls, hint coalescing, endpoint generations, and version-mismatch reporting. |
|
||||||
|
| `peer_db` | Authenticated endpoint/state records and exact `content_id` source lookup. |
|
||||||
|
| download/storage/stream install | Validated catalog plan, hash-as-received, source quarantine/retry, sentinel commit, and protected staging. |
|
||||||
|
| `call_to_play` | Local author slice, per-peer replacement snapshots, simple authority checks, timers, and bounds. |
|
||||||
|
| Tauri/frontend | Global sharing switch, verification/progress failures, incompatible-version notice, and replacement of the full derived Call-to-Play view. |
|
||||||
|
| peer CLI | Distinct deterministic identities, hostile TLS/content modes, and zero-prompt multi-peer scenarios. |
|
||||||
|
|
||||||
`lanspread-proto` owns wire-only fixed types, explicit serializers, transcript
|
## 6. Implementation phases and gates
|
||||||
builders, `PeerEndpoint`, `SignedEnvelope`, and signed Call-to-Play wire data.
|
|
||||||
|
|
||||||
Principal `lanspread-peer` changes:
|
Every code phase runs `just fmt`, `just clippy`, and `just test`. Frontend or
|
||||||
|
Tauri phases also run `just frontend-test` and `just build`. Network/transfer
|
||||||
|
phases run focused peer-CLI scenarios during development and the unfiltered
|
||||||
|
`just peer-cli-tests` before completion. Manual alpha/bravo/charlie evidence
|
||||||
|
must use a freshly built image.
|
||||||
|
|
||||||
| Area | Required change |
|
### Phase 1 — land filesystem confinement immediately
|
||||||
|---|---|
|
|
||||||
| `identity.rs`, startup | Durable derived identity and typed repair; no silent UUID regeneration |
|
|
||||||
| `config.rs`, TLS assets | Remove shared `CERT_PEM`/`KEY_PEM` and repository key material after Phase 2 |
|
|
||||||
| `network.rs`, remote peer helpers | Mandatory expected endpoint; remove address-derived identity |
|
|
||||||
| `services/server.rs` | Per-peer rustls TLS server and bounded candidate/connect-back work; generic accept-loop DoS remains an accepted limit |
|
|
||||||
| `services/discovery.rs` | Candidate directory/retry only; no authenticated-state mutation |
|
|
||||||
| `services/handshake.rs` | Non-authoritative inbound Hello; pinned callback; record dialled endpoint |
|
|
||||||
| `services/stream.rs` | Envelope verification boundary and `VerifiedSender`; no payload-trusted IDs or `Goodbye` |
|
|
||||||
| `services/liveness.rs` | Pinned endpoint probes and revision-conditional update/removal |
|
|
||||||
| `peer_db.rs` | Atomic verified bind/collision rejection; first-class endpoint/index invariants |
|
|
||||||
| metadata/download/stream install | Allowed-source manifest provenance through selection, storage, retries, and revocation |
|
|
||||||
| `call_to_play.rs` | Signed event store, CallRef authority, canonical rootless terminals, exact cache, count/byte/reserve policy |
|
|
||||||
| errors/events | Typed auth, identity-repair, source-denied, and overload states reaching the UI |
|
|
||||||
|
|
||||||
The peer CLI gains deterministic explicit identities, full endpoint input, and
|
Implement `ValidatedDownloadManifest`, make the UI submit only `game_id`, and
|
||||||
hostile modes. Tauri gains minimal Phase 1 repair and Phase 2 source-approval
|
move all validation before transaction/storage mutation. Centralize reserved
|
||||||
surfaces before the complete trust UI.
|
paths and add the zero-mutation hostile tests from §3.1. Preserve current wire
|
||||||
|
bytes in this phase; it is an independent safety fix.
|
||||||
|
|
||||||
## 10. UI and operational semantics
|
Gate: standard Rust/Tauri checks, hostile descriptor tests, full peer-CLI suite,
|
||||||
|
and supported Windows path/reparse evidence. Linux-only results must not be
|
||||||
|
reported as Windows proof.
|
||||||
|
|
||||||
- **Identity repair (Phase 1):** typed failure, retry/unlock, import backup,
|
### Phase 2 — establish real catalog content authority
|
||||||
explicit backend selection where safe, deliberate file-backed fresh create,
|
|
||||||
and deliberate reset. Networking stays stopped during repair.
|
|
||||||
- **Source approval (with enforcement):** show full/short fingerprint and
|
|
||||||
“Allow downloads from this device.” New devices default to prompt. The UI
|
|
||||||
must not call this content trust.
|
|
||||||
- **Peer identity:** display name + short fingerprint, persistent New marker
|
|
||||||
until acknowledged, and a prominent same-name/new-key conflict.
|
|
||||||
- **Blocking:** immediate durable block/unblock, direct and relayed enforcement,
|
|
||||||
visible recomputation, and resync on unblock. Unblock does not restore source
|
|
||||||
permission automatically.
|
|
||||||
- **Identity settings:** full copyable fingerprint, honest backend label,
|
|
||||||
backup/import/reset consequences, and platform-specific persistence wording.
|
|
||||||
- **Call-to-Play overload:** persistent typed “history limit reached; updates
|
|
||||||
may be incomplete” warning, signed offending authors when known, and reset /
|
|
||||||
restart recovery guidance.
|
|
||||||
- **Diagnostics:** rejected auth/source objects surface typed reason codes and
|
|
||||||
fingerprints, never secret material or misleading generic network errors.
|
|
||||||
|
|
||||||
## 11. Implementation phases and acceptance gates
|
Add the reproducible content-manifest generator and fixture manifests. Freeze
|
||||||
|
the versioned manifest/content-ID encoding with golden tests. Extend local
|
||||||
|
catalog state, build download plans only from that state, implement streaming
|
||||||
|
SHA-256 checks and source quarantine, and implement verified extracted
|
||||||
|
manifests for Stream Install.
|
||||||
|
|
||||||
Every code phase runs `just fmt`, `just clippy`, and `just test`. UI/Tauri
|
Do not claim completion from test fixtures alone: production catalog packages
|
||||||
phases also run `just frontend-test` and `just build`. Every protocol/peer phase
|
must have independently generated manifests, and the release/build path must
|
||||||
runs targeted `just peer-cli-tests` during development and the unfiltered suite
|
reject a missing manifest. Benchmark hashing at normal LAN throughput.
|
||||||
before phase completion. Wire phases run the honest alpha/bravo/charlie matrix
|
|
||||||
using a freshly built image.
|
|
||||||
|
|
||||||
Fix the `justfile` stale-image hazard first: manual `peer-cli-run`, alpha,
|
### Phase 3 — prove and implement simple responder identity
|
||||||
bravo, and charlie recipes must depend on a readiness target that builds the
|
|
||||||
current `peer-cli-image` and network. A manual run against an old image is not
|
|
||||||
acceptance evidence.
|
|
||||||
|
|
||||||
### Phase 0 — freeze contracts and complete spikes
|
Start with a bounded rustls/s2n-quic spike that proves self-issued certificate
|
||||||
|
support, SPKI extraction, expected-ID pinning, and real TLS 1.3
|
||||||
|
CertificateVerify. The certificate-A/private-key-B negative is the go/no-go
|
||||||
|
gate. Choose Ed25519 or P-256 based on that proof, using one TLS identity key.
|
||||||
|
|
||||||
Record in the repository:
|
Then add simple load-or-generate persistence, deterministic CLI identities,
|
||||||
|
`PeerEndpoint`, and endpoint plumbing through every outbound consumer. Separate
|
||||||
|
mDNS candidates from authenticated peer records and make liveness removal
|
||||||
|
generation-conditional. No trust database or identity UI is introduced.
|
||||||
|
|
||||||
- the identity transition table and backend scope;
|
### Phase 4 — make the single wire cutover
|
||||||
- exact backup-envelope KDF/AEAD/header parameters;
|
|
||||||
- `PeerEndpoint`, socket-admissibility, candidate/verified-record, clone, and
|
|
||||||
liveness-generation invariants;
|
|
||||||
- candidate TTL/backoff and all candidate/live-peer/connect-back/trust/nonce
|
|
||||||
capacity and rate values;
|
|
||||||
- trust/source-permission schema and block transitions;
|
|
||||||
- exact envelope and event transcripts;
|
|
||||||
- the protocol 8/9/10 dispatch matrix (protocol 8 no network CTP or
|
|
||||||
client-authoritative controls; protocol 9 self-signed CTP only; protocol 10
|
|
||||||
envelope-authenticated controls);
|
|
||||||
- Call-to-Play count/byte/reserve values and accepted exhaustion behavior;
|
|
||||||
- manifest count/size bounds, Windows/path policy, and control/snapshot
|
|
||||||
frame/pagination decision;
|
|
||||||
- maximum-size history/manifest and honest download/handshake workloads plus
|
|
||||||
objective peak-memory, latency, and throughput-regression thresholds for the
|
|
||||||
final performance gate;
|
|
||||||
- the dependency rule: `lanspread-identity` depends on `lanspread-proto`, never
|
|
||||||
the reverse; and
|
|
||||||
- the completed RFC 7250 RPK / Ed25519 X.509 spike result, disabled
|
|
||||||
resumption/0-RTT configuration, and exact legal fallback if one is needed.
|
|
||||||
|
|
||||||
Phase 1a may run in parallel. Its identity primitives need only the frozen
|
Bump the current protocol once and activate all coupled wire behavior from
|
||||||
identity/transcript slice; persistent writes and runtime identity replacement
|
§4: pinned transport, catalog `content_id`, catalog-driven downloads, pull-only
|
||||||
wait for the storage table, while unrelated TLS/manifest measurements may
|
library synchronization, bounded invalidation hints, author-owned
|
||||||
finish concurrently.
|
Call-to-Play snapshots, and no `Goodbye`.
|
||||||
|
|
||||||
### Prerequisite — download manifest confinement
|
This phase is not complete until:
|
||||||
|
|
||||||
After Phase 0 freezes manifest bounds, implement §6 with no wire change and
|
- three fresh peers discover each other with no prompts and see post-start
|
||||||
before Phase 1b writes persistent identity. Phase 1a may remain in parallel.
|
library changes;
|
||||||
Mandatory hostile path, limit, absent-from-authoritative-selection, and
|
- a new peer reconstructs active Call-to-Play state by pulling every live
|
||||||
zero-mutation tests land here, including the cross-game `local/` sentinel. Run
|
author, and creator departure removes the call;
|
||||||
the full peer-CLI suite and `just build` because the Tauri-to-download
|
- every metadata, chunk, retry, stream-install, healing, liveness, and direct
|
||||||
integration is in the path. Windows alias/reparse behavior requires supported
|
CLI dial rejects the wrong key at the expected address;
|
||||||
Windows CI or recorded manual evidence; Linux-only results are labelled as
|
- a forged mDNS record or inbound hint cannot create/rebind/remove peer state,
|
||||||
such rather than generalized.
|
inject a library/Call-to-Play update, or bypass a pinned pull;
|
||||||
|
- an honest multi-source download swarms automatically and commits only the
|
||||||
|
catalog bytes;
|
||||||
|
- one bad source is quarantined and another source completes the chunk;
|
||||||
|
- all-bad/only-bad sources fail without committing `version.ini` or touching
|
||||||
|
`local/`;
|
||||||
|
- a streamed path/hash/set mismatch cannot promote staging;
|
||||||
|
- an oversized Call-to-Play snapshot affects only that remote author and local
|
||||||
|
publication still works; and
|
||||||
|
- protocol-7 peers are rejected while the UI receives enough information to
|
||||||
|
explain the version mismatch.
|
||||||
|
|
||||||
### Phase 1a — primitives and fake-backed groundwork
|
Update `ARCHITECTURE.md`, protocol docs, and CLI documentation in the same
|
||||||
|
phase; do not leave the shared-certificate or relayed-event description behind.
|
||||||
|
|
||||||
Add the identity crate, Ed25519/PeerId/signature primitives, versioned
|
### Phase 5 — finish the small user-facing surface and audit
|
||||||
zeroizing secret type, normalized backend outcomes, fake backend, fixed-seed
|
|
||||||
golden transcript/signature vectors, secret-redaction tests, and
|
|
||||||
keyring/platform feasibility. Golden vectors include exact control request and
|
|
||||||
correlated response bytes/signatures plus one CTP event and assert canonical
|
|
||||||
base64url string fields. Certificate DER/cross-language vectors are not
|
|
||||||
required. No persistent writes, runtime-ID replacement, legacy deletion, or
|
|
||||||
trust population.
|
|
||||||
|
|
||||||
### Phase 1b — durable identity and usable repair, no wire change
|
Add the global sharing switch and the concrete progress/error states from
|
||||||
|
§3.6. Run a first-run test with an empty app-data directory, a normal restart,
|
||||||
|
a corrupt identity file, and unwritable identity persistence; none may produce
|
||||||
|
a key-management workflow or prevent the ephemeral fallback from participating
|
||||||
|
for that run.
|
||||||
|
|
||||||
Implement every §7.2 transition-table cell, lifetime lease, keyring/file
|
Run all standard checks, the complete peer-CLI suite, fresh three-peer manual
|
||||||
backends, secret-first sidecar recovery, override isolation, correct Tauri
|
scenarios, production builds/bundles on supported platforms, and a final audit
|
||||||
`app_data_dir`, encrypted backup/import/reset, and the minimal repair UI. Keep
|
for:
|
||||||
the protocol-7 runtime/advertised UUID until the Phase 2 wire cutover; do not
|
|
||||||
populate security trust from it.
|
|
||||||
|
|
||||||
Mandatory tests include every backend state, no-fallthrough on
|
- raw remote manifests reaching storage;
|
||||||
locked/denied/unavailable/corrupt, sidecar mismatch, absent/corrupt sidecar
|
- unhashed transfer completion or CRC32 presented as malicious-source proof;
|
||||||
recovery, unsupported-version zero writes, valid-sidecar no unselected probe,
|
- the shared repository TLS private key;
|
||||||
multiple secrets, write-failure reprobe, crash after secret before sidecar,
|
- address-only outbound connections or fabricated peer IDs;
|
||||||
replacement mismatch repair, every backend-migration intent stage and target
|
- direct mutation from mDNS, inbound Hello, deltas, or change hints;
|
||||||
conflict, account-wide lease held throughout repair, two
|
- relayed third-party Call-to-Play history or permanent tombstones;
|
||||||
simultaneous starts, file permissions/atomic replacement, CLI-over-environment
|
- signed control envelopes, nonce/replay tables, keyring/backup/repair code, or
|
||||||
precedence and zero default-store access in explicit modes,
|
per-peer source authorization reappearing without a new product requirement;
|
||||||
tampered/wrong-passphrase backup, selected-backend reset/import failure without
|
- silent protocol-version failure; and
|
||||||
fallback, successful export/import fingerprint round trip, header/decoded-size
|
- user wording that calls a peer, display name, or executable “trusted” merely
|
||||||
and KDF-cost bounds rejected before expensive allocation/work with zero writes,
|
because TLS or a hash check succeeded.
|
||||||
runtime stop/rebuild/restart on replacement, legacy UUID preservation
|
|
||||||
on every failed path, and successful import/reset still advertising the same
|
|
||||||
protocol-7 UUID/shared TLS identity rather than prematurely activating the new
|
|
||||||
key, secret redaction, and “shell visible/network stopped”
|
|
||||||
repair behavior. Give every Docker/peer-CLI recipe a distinct deterministic
|
|
||||||
seed/file before this gate; test restart stability and no accidental shared
|
|
||||||
identity. Run `just frontend-test`, `just build`, and the unfiltered peer-CLI
|
|
||||||
suite. Real supported keyring/app-data backends require platform CI or recorded
|
|
||||||
manual evidence.
|
|
||||||
|
|
||||||
### Phase 2 — responder-authenticated transport and endpoints (protocol 8)
|
## 7. Success criteria
|
||||||
|
|
||||||
Land the selected RPK/X.509 mode, real CertificateVerify, mandatory expected
|
The plan is complete when the following statement is true from a user's point
|
||||||
endpoint, all endpoint plumbing, mDNS candidate separation/retry,
|
of view:
|
||||||
non-authoritative inbound Hello, bounded/deduplicated connect-back, atomic
|
|
||||||
collision rejection, removal of address fallbacks and `Goodbye`, and
|
|
||||||
revision-conditional liveness. Delete the shared repository certificate/key and
|
|
||||||
compiled constants regardless of which selected per-peer TLS mode lands.
|
|
||||||
After the durable identity loads successfully, protocol 8 activates its derived
|
|
||||||
PeerId for runtime/advertising and deletes the legacy UUID as part of this
|
|
||||||
versioned cutover—not earlier.
|
|
||||||
|
|
||||||
Client-originated controls remain unauthenticated. Library/legacy notifications
|
> I opened Lanspread at a LAN party, immediately saw the people and games
|
||||||
cause only bounded pinned resync. Protocol 8 disables all network Call-to-Play
|
> nearby, downloaded from all matching peers without approving devices, and
|
||||||
ingestion/sync; local CTP remains available and Phase 3 restores networking
|
> Lanspread itself rejected any wrong data. I never had to know that it owns a
|
||||||
with self-authenticating events. The phase-current scenario matrix explicitly
|
> TLS key.
|
||||||
expects this temporary gate instead of retaining contradictory S48/S49
|
|
||||||
third-party-relay expectations.
|
|
||||||
|
|
||||||
Mandatory negatives: cert/SPKI A with private key B, wrong expected ID,
|
From the implementation point of view, that experience rests on only three
|
||||||
disallowed/mismatched RPK or X.509 mode, resumption/0-RTT disabled, copied
|
security boundaries: confined local paths, catalog-owned content hashes, and
|
||||||
public-key inbound claim, invalid callback socket class/source-IP, every
|
responder-pinned TLS. Call to Play deliberately reuses the pinned-pull model
|
||||||
per-source/per-target/global callback and candidate-cap axis, advertised Ack
|
and remains ephemeral instead of becoming a second distributed security
|
||||||
ID/key mismatch and address redirect, callback/resync terminal behavior with no
|
protocol.
|
||||||
recursive callback, ALPN/version mismatch, address collision
|
|
||||||
with intact indexes, simultaneous same-key clone,
|
|
||||||
mDNS-only mutation, candidate expiry/retry without a new mDNS event, stale ping
|
|
||||||
or timeout-prune after same-address activity/rebind/reconnect, inbound traffic
|
|
||||||
not refreshing an unreachable listener, wrong key at reused address, and no
|
|
||||||
address-derived fallback. If P-256 is selected, also mutate/remove every
|
|
||||||
pre-channel binding field/signature/SPKI and reject a Hello-only binding.
|
|
||||||
The resumption negative performs a second connection/early-data attempt and
|
|
||||||
proves the full verifier and CertificateVerify path runs again. A flood mixing
|
|
||||||
Hello, mDNS, and legacy library hints proves the unified scheduler remains
|
|
||||||
within every budget.
|
|
||||||
|
|
||||||
Send forged candidate Hello bodies containing library/CTP data and forged
|
|
||||||
legacy LibraryDelta/CTP payloads; they must not merge, create/rebind/remove a
|
|
||||||
peer, or refresh liveness. Only the separately pinned resync response may
|
|
||||||
merge. Protocol-8 Hello's candidate-only decoder rejects the obsolete full
|
|
||||||
state shape before retaining it.
|
|
||||||
|
|
||||||
Positive tests prove pinned resync propagates post-start library changes, a
|
|
||||||
genuinely dead current endpoint eventually emits `PeerLost` and applies the
|
|
||||||
defined active-download cancellation, and every endpoint consumer (metadata,
|
|
||||||
chunks, retry, streamed install, healing, liveness, direct CLI) pins the
|
|
||||||
captured ID. Import/reset under per-peer TLS stops networking, rebuilds the
|
|
||||||
derived ID/certificate/advertisement, rejects the old ID/key, and reconnects
|
|
||||||
under the new identity. The former Goodbye shutdown scenario is rewritten around liveness.
|
|
||||||
Run `just build`, the unfiltered phase-current peer-CLI suite, and fresh
|
|
||||||
three-peer matrix. Update the relevant architecture, threat, and CLI docs in
|
|
||||||
this phase.
|
|
||||||
|
|
||||||
### Phase 2b — default-deny transfer sources and minimal approval UI
|
|
||||||
|
|
||||||
Using Phase 2 identities, implement the durable trust/source state and outbound
|
|
||||||
source-enforcement portions of §§7.3 and 7.10: approved-only raw-manifest
|
|
||||||
bounds/consensus, ordinary exact provenance, streamed root-archive eligibility
|
|
||||||
and staging validation, and mid-transfer revocation. Public browse remains
|
|
||||||
open; inbound serving remains anonymous/public until Phase 4, so requester
|
|
||||||
blocking is not claimed here. Relayed-author blocking belongs to Phase 3.
|
|
||||||
|
|
||||||
Mandatory tests: a prompt/denied sole source creates nothing; browse still
|
|
||||||
works; approval enables download; unapproved metadata cannot enter transfer
|
|
||||||
consensus; UI selection absent from approved manifests fails; wrong identity at
|
|
||||||
a reused address fails; retry stays within exact attesters; manifest mismatch
|
|
||||||
fails before mutation; and deny/block cancels and cleans both ordinary and
|
|
||||||
streamed transfers without touching `local/`. Streamed hostile tests require
|
|
||||||
complete root-archive attestation and reject extracted traversal/reserved/
|
|
||||||
symlink-reparse paths, count/byte overflow including a late invalid entry, and
|
|
||||||
invalid `Complete` without promoting any staging. Also test raw count/byte limits,
|
|
||||||
failed security-state writes/restart, and that same-name/new-key or address
|
|
||||||
reuse by a different key never inherits `allowed`, while the same verified
|
|
||||||
PeerId moving to a newly pinned address retains it; existing honest download
|
|
||||||
scenarios explicitly approve their sources. Saturate total trust records,
|
|
||||||
observed-name history, and unacknowledged observations and prove that
|
|
||||||
acknowledged, blocked, and allowed records are never silently evicted. Run
|
|
||||||
frontend/build and full peer-CLI gates, and update
|
|
||||||
source-permission/user-facing docs here.
|
|
||||||
|
|
||||||
Catalog-matching already-downloaded data is explicitly tested for offline
|
|
||||||
install/reinstall and local serving with no remote approval, so source policy
|
|
||||||
cannot accidentally gate local provenance.
|
|
||||||
|
|
||||||
### Phase 3 — independently signed Call-to-Play objects (protocol 9)
|
|
||||||
|
|
||||||
Implement §7.9, including core-created CallRef/IDs, verbatim relay, creator and
|
|
||||||
participant authority, canonical rooted/rootless terminal state, deterministic
|
|
||||||
horizon checks, exact-object cache, block-at-merge, hard count/byte/author
|
|
||||||
limits, local reserve, dependency-closed classification, and overload UI.
|
|
||||||
Third-party relay is restored only for valid signed objects.
|
|
||||||
|
|
||||||
Mandatory tests cover wrong creator/participant, CallRef mismatch, competing
|
|
||||||
same-CallRef Creates under arrival/group/partition permutations, fresh-store
|
|
||||||
rootless terminal, noncreator tombstone, rooted/rootless terminal permutations
|
|
||||||
and partitions, later-arriving earlier winner, stale resurrection, 15-minute
|
|
||||||
compaction, rootless AddTime/participant `NeedHistory`, rootless no-display and
|
|
||||||
later hydration, old and two-day history, timestamp equality/overflow/ready-at
|
|
||||||
and exact ECMAScript-range boundaries, AddTime/Create recomputation under
|
|
||||||
arrival/group/partition permutations, bytewise Rust/TypeScript ordering, and
|
|
||||||
receiver-clock-independent three-day rules, every
|
|
||||||
body/key/signature/ID/protocol-version mutation, observed-ID preseed, invalid signature after
|
|
||||||
cache hit/compaction, bounded cache, count/byte/per-author/tombstone/Sybil
|
|
||||||
limits, local reserve, terminal at capacity, blocked author relayed by a
|
|
||||||
nonblocked peer regardless of that relay's source permission, blocking
|
|
||||||
already-retained creator and participant state,
|
|
||||||
quarantine accounting, unblock authenticated resync, mixed
|
|
||||||
invalid/blocked/over-quota author isolation, all-or-none collective global
|
|
||||||
overflow, over-quota Create removing its under-quota dependent while preserving
|
|
||||||
an unrelated call, sequential-versus-single-batch arrival where an initially
|
|
||||||
inactive action becomes effective after an earlier canonical Create,
|
|
||||||
same-batch fixed-point dependency closure, bounded snapshot assembly, and
|
|
||||||
explicit bounded-divergence recovery. Frontend tests prove chat uses enclosing
|
|
||||||
event ID and rootless state stays hidden. Saturate relay-only author metadata
|
|
||||||
and prove it remains ephemeral and bounded without growing the durable trust
|
|
||||||
store. Run `just frontend-test`, `just
|
|
||||||
build`, the full hostile peer-CLI suite (restoring late-join/relay scenarios),
|
|
||||||
and honest matrix. Update CTP architecture and UI claims here.
|
|
||||||
|
|
||||||
### Phase 4 — signed control envelopes (protocol 10)
|
|
||||||
|
|
||||||
Implement the exact §7.8 envelope, explicit byte serializers, request/response
|
|
||||||
nonce correlation, boundary verification, `VerifiedSender`, removal of
|
|
||||||
payload identity fields, bounded duplicate suppression, and control frame cap
|
|
||||||
or pagination. Reject a blocked `VerifiedSender` before every inner handler,
|
|
||||||
including Hello, browse, and download serving; associate long-lived streamed
|
|
||||||
and ordinary chunk serving with that identity and cancel every active outbound
|
|
||||||
serve operation if the identity becomes blocked.
|
|
||||||
|
|
||||||
Mandatory tests mutate every signed field and cover unsigned frames, wrong
|
|
||||||
signer/derived ID, protocol, recipient, context, pinned response sender,
|
|
||||||
request nonce, missing/unknown fields, padded/wrong-alphabet base64,
|
|
||||||
numeric-array bytes, wrong decoded lengths, explicit base64 round trip,
|
|
||||||
concurrent duplicates, sender/context isolation, invalid-signature nonce-cache
|
|
||||||
poisoning, eviction with idempotent replay, oversized frame/payload, forged
|
|
||||||
library delta, every valid blocked state-changing sender before handler
|
|
||||||
dispatch, blocked Hello/read/stream serving and unsigned-Hello bypass, block
|
|
||||||
cancellation of active ordinary-chunk and streamed serving, and liveness
|
|
||||||
attribution. Honest
|
|
||||||
library, download, and event flows
|
|
||||||
must remain green. Run `just build`, the full peer-CLI suite, and fresh
|
|
||||||
three-peer matrix. Update protocol/trust documentation here.
|
|
||||||
|
|
||||||
### Phase 5 — complete trust and identity UX
|
|
||||||
|
|
||||||
Add polished identity/pin/new/reviewed/name-conflict displays, source permission
|
|
||||||
management, block/unblock, honest storage labels, and refined repair/backup
|
|
||||||
flows. Source enforcement, relayed-object blocking, and direct requester
|
|
||||||
blocking already exist in Phases 2b, 3, and 4 respectively; this phase improves
|
|
||||||
visibility and management.
|
|
||||||
|
|
||||||
Test reducers and Tauri failure paths, immediate sensitive-state durability,
|
|
||||||
relayed blocking, existing-state recomputation, unblock resync, permission
|
|
||||||
persistence, same-name/new-key behavior, duplicate identity warnings, and proof
|
|
||||||
that unblock leaves download permission denied. Saturation must be visible;
|
|
||||||
the user can deliberately remove an eligible unacknowledged record without
|
|
||||||
evicting protected state, after which an honest new identity can be admitted
|
|
||||||
and approved. Run frontend/build and full peer-CLI gates. Update UX
|
|
||||||
documentation in the same phase.
|
|
||||||
|
|
||||||
### Phase 6 — optional hardening
|
|
||||||
|
|
||||||
Separately designed features may include dual-signed key rotation, party
|
|
||||||
admission, connection retry/address tokens, and deterministic over-quota author
|
|
||||||
quarantine. Each feature owns a protocol bump and hostile tests if it lands.
|
|
||||||
Before implementation it freezes its own contract; its gate includes positive
|
|
||||||
end-to-end behavior, applicable persistence/failure/recovery cases, negative
|
|
||||||
security cases, and same-phase documentation. A reject-all implementation does
|
|
||||||
not satisfy the gate.
|
|
||||||
|
|
||||||
Catalog content hashes/signatures remain separate, non-optional future work,
|
|
||||||
not a claim completed by this phase.
|
|
||||||
|
|
||||||
### Phase 7 — final audit, documentation, and performance
|
|
||||||
|
|
||||||
This is not the first hostile-test or documentation phase. Audit the already
|
|
||||||
updated `ARCHITECTURE.md`, README files, threat claims, peer-CLI docs, and UI
|
|
||||||
wording; run every standard gate,
|
|
||||||
unfiltered `just peer-cli-tests`, a fresh three-peer matrix, and download/
|
|
||||||
handshake/full-history performance checks against Phase 0's fixed workloads and
|
|
||||||
failure thresholds. Build supported-platform production bundles with
|
|
||||||
`just bundle` and smoke-test packaged startup, identity/keyring/app-data
|
|
||||||
selection, repair, and restart; the no-bundle output from `just build` alone is
|
|
||||||
not shipping-artifact evidence.
|
|
||||||
|
|
||||||
Audit for absence of the shared TLS key, `Goodbye`, address-only connects,
|
|
||||||
`ensure_peer_id_for_addr`, fabricated IDs, payload-trusted sender fields,
|
|
||||||
unbounded auth/CTP inputs, deferred negative tests, and any “verified/trusted
|
|
||||||
content” wording.
|
|
||||||
|
|
||||||
## 12. Dependencies
|
|
||||||
|
|
||||||
Prefer the existing AWS-LC stack.
|
|
||||||
|
|
||||||
| Crate | Purpose | Constraint |
|
|
||||||
|---|---|---|
|
|
||||||
| `rustls` 0.23 | Full verifier/config construction | Version aligned with s2n-quic-rustls |
|
|
||||||
| `aws-lc-rs` | Ed25519, SHA-256, KDF/AEAD as needed | Existing crypto backend |
|
|
||||||
| `rcgen` if X.509 is selected | Self-issued leaf generation | `default-features = false`, AWS-LC feature only |
|
|
||||||
| `x509-parser` if needed | Strict SPKI/binding extraction | Parsing only; reject unsupported shapes |
|
|
||||||
| `keyring` 3 | OS secret stores | Normalize locked/denied/unavailable distinctly |
|
|
||||||
| `zeroize` | Secret hygiene | Seed-bearing values only |
|
|
||||||
| `data-encoding` or existing equivalent | base32/base64url | One exact encoding implementation |
|
|
||||||
|
|
||||||
`s2n-quic` uses the rustls provider and retains only required provider features.
|
|
||||||
The new crate keeps `unsafe_code = "forbid"`.
|
|
||||||
|
|
||||||
## 13. Explicit risks and decisions
|
|
||||||
|
|
||||||
- **TLS mode uncertainty:** Phase 2 does not begin implementation around an
|
|
||||||
assumed raw-key path; Phase 0 chooses RPK or Ed25519 X.509 and proves the
|
|
||||||
wrong-private-key feasibility first.
|
|
||||||
- **Linux keyring availability:** unavailable is not absent. Fresh GUI users
|
|
||||||
can deliberately choose a labelled file backend; headless runs choose an
|
|
||||||
explicit identity source.
|
|
||||||
- **Identity loss and clones:** backup/import are necessary recovery tools but
|
|
||||||
can clone a key. The UI cannot claim automatic clone revocation.
|
|
||||||
- **Endpoint plumbing size:** this is real cross-cutting work, not hidden behind
|
|
||||||
`connect_to_peer(expected)`. Phase 2 inventories every caller and tests
|
|
||||||
retries/streamed installs explicitly.
|
|
||||||
- **Authenticated does not mean harmless:** self-signed identities and source
|
|
||||||
approval still leave approved malicious content and link/application-task
|
|
||||||
DoS, including the explicitly accepted generic server-fan-out risk in §4.
|
|
||||||
- **Bounded state versus Byzantine convergence:** this plan chooses finite
|
|
||||||
memory and explicit overload over distributed consensus machinery for a
|
|
||||||
noncritical LAN-party feature.
|
|
||||||
- **Permanent tombstones:** their anti-resurrection purpose is preserved. They
|
|
||||||
are bounded through admission, never arrival-ordered eviction.
|
|
||||||
- **Scope pressure:** each phase owns its negative tests and safe intermediate
|
|
||||||
restrictions. No phase gets security credit for a later phase's mechanism.
|
|
||||||
|
|
||||||
## 14. Closure of prior review findings
|
|
||||||
|
|
||||||
| Finding | Resolution in this plan |
|
|
||||||
|---|---|
|
|
||||||
| F1 signed listener/address collision | §§7.4–7.6: dialled endpoint authority, non-authoritative claims, pinned connect-back, atomic conflict rejection without eviction. |
|
|
||||||
| F2 responder-only Phase 2 / relayed authority gap | §§7.6, 7.9 and phases 2–4: protocol 8 disables network CTP and treats client claims as hints; protocol 9 admits only independently signed CTP objects; all other client authority waits for envelopes. |
|
|
||||||
| F3 compacted tombstone creator proof | §7.9: CallRef in every body, creator-signed rootless burn certificates, canonical retained rootless terminal, fresh-peer propagation. |
|
|
||||||
| F4 arbitrary event IDs/cache bypass | §7.9: full content-derived IDs and bounded exact-signed-object cache. |
|
|
||||||
| F5 invalid ±10-minute history rule | §7.9: no receiver-age rejection; deterministic three-day field horizon and local-only publication skew policy. |
|
|
||||||
| F6 resource exhaustion | §§4, 7.6, 7.8, 7.9: concrete new-amplification/batch/store/cache bounds, author quotas, local reserve, rejection/no tombstone eviction, and honest exhaustion behavior. The review's comprehensive connection/stream/disk-read scheduler is explicitly not adopted and remains an accepted separate availability risk. |
|
|
||||||
| F7 replay/Goodbye | §§7.7–7.8: delete Goodbye, narrow duplicate suppression claim, local recipient/context/pinned response, conditional liveness removal. |
|
|
||||||
| F8 storage crash/fallback state | §7.2 and Phase 1: explicit transition table, conclusive absence, lifetime lease, secret-first derivative sidecar, early repair, correct GUI path. |
|
|
||||||
| F9 expected identity not threaded | §7.4 and Phase 2: first-class endpoint through all consumers; delete address-derived/fabricated identity and require CLI fingerprint. |
|
|
||||||
| F10 trust/block/lifecycle | §§7.2–7.3, 7.9–7.10: separate review/block/source states, durable writes, relay blocking, Phase 1 repair, honest backup/clone semantics. |
|
|
||||||
| F11 incomplete crypto/wire contract | §§7.1, 7.5, 7.8–7.9: exact transcripts/encodings/crate direction, real CertificateVerify, wrong-key test, AWS-LC rcgen, pre-channel fallback binding. |
|
|
||||||
| F12 late tests/docs | §11: phase-owned negatives, full peer-CLI gates, GUI build gates, fresh manual images, final-audit-only Phase 7. |
|
|
||||||
|
|
||||||
Additional dialogue findings are also closed: §6 handles the live cross-game
|
|
||||||
`local/` truncation bug; §7.7 handles the stale liveness-probe race; §7.9
|
|
||||||
handles rootless terminal ordering and chat `message_id`; §§7.3 and 7.10 add
|
|
||||||
default-deny source admission without misrepresenting it as content integrity.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user