Versioned documentPlain text

Private Thread trial profile v1

This is an experimental application profile around MLS. It is not yet part of Skein Core or the public node installer. The implementation lives in this directory.

On this page

Identity and membership

A Thread ID is 32 random bytes. The creator's Ed25519 public key is its initial administrator and member. Participants MUST obtain the Thread ID and administrator key through a trusted channel; an invitation cannot establish its own trust anchor. The MLS BasicCredential identity MUST equal its Ed25519 signature key.

Membership changes use src/thread_membership.rs: the administrator signs an Add or Remove; an Add also requires the invited identity's consent. A departing member signs Leave. The administrator still produces the MLS commit for a departure. Administrator replacement and administrator departure are unsupported.

The membership log and MLS roster MUST agree after every transition. Each MLS commit carries exactly one membership change; the MLS epoch MUST equal the policy revision. Independent key-update commits are not supported by this profile. Stale or conflicting changes are refused. The administrator can fork a group; this profile does not provide consensus or an honest-majority guarantee.

Encoding and signatures

The ciphersuite is MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519. MLS messages and KeyPackages use exact TLS serialization. JSON is the experimental CLI/HTTP wrapper. Byte arrays are JSON arrays of integers 0–255. IDs are 32 bytes. Unknown fields in typed input objects are rejected. CLI path arguments use 64 hexadecimal characters.

A Welcome bundle contains thread, administrator, changes, welcome, and signature. Its Ed25519 signature covers the UTF-8 bytes of SkeinNet/PrivateThreadWelcome/v1 followed by a zero byte and the compact JSON tuple [thread,administrator,changes,welcome]. Change objects have this field order: thread,previous,revision,action,member,signature,consent. Compact JSON has no whitespace and uses decimal unsigned integers; actions are add, remove, leave. The recipient verifies that signature, replays the signed history, imports the Welcome, checks the Thread and roster, and verifies that the final Add names itself.

A delivery receipt contains thread, recipient, wire_hash, and signature. wire_hash is SHA-256 of the exact MLS packet. Its Ed25519 signing input is SkeinNet/PrivateThreadReceipt/v1, a zero byte, then the three 32-byte values in that order. Receipts are issued only after the receiving state is saved. They acknowledge durable local receipt, not agreement with or attention to a message.

Application plaintext is a signed Skein Event. Its author MUST match the authenticated MLS sender and its Thread MUST match the MLS Group ID. The author MUST still be a current member, including when decrypting an older-epoch packet.

Persistence and delivery

The CLI owns a private directory with an encrypted identity and one encrypted checkpoint per Thread. The local wrapping key is in that directory; filesystem permissions, rather than encryption alone, protect a compromised account.

Joining writes an encrypted redo journal containing the resulting group and the identity with its pending KeyPackage retired. Recovery completes retirement and group creation before any normal operation. The journal is then removed and the directory synchronized. Exact Welcome retries return the same signed receipt. Whole-directory rollback is not detected. Tests cover process interruption, not arbitrary hardware or filesystem failure.

An outgoing batch remains saved until every target's signed receipt is recorded. Each sender retains one active batch and up to 16 queued messages, with a combined 1 MiB ciphertext cap. Acknowledging a batch durably promotes the next message. group deliver makes one finite pass, using curl with a five-second deadline per peer and at most 32 KiB per response. Retry reruns that command; completed recipients are skipped. A successful pass can leave later messages queued; complete is true only when the queue is empty. A Welcome is imported locally by its recipient, which returns its signed receipt to the administrator.

Messages and membership commits use POST /message and /commit. Message bodies are packet byte arrays; commit bodies contain change and wire. Success is HTTP 200 with committed and a signed receipt. Refused requests return 400; a busy worker returns 429; oversized bodies return 413. No remote endpoint exposes keys, plaintext, joining, sending, or acknowledgement of someone else's outbox. Public ingress MUST run behind TLS. Transport alone cannot forge a recipient receipt.

Bounds and failure behavior

The profile caps membership at 128, membership history at 256 changes, local Event history at 256 Events/2 MiB, packets at 256 KiB, and encrypted snapshot plaintext at 8 MiB. Up to 256 received-packet receipts are retained without eviction; when full, new packets are refused while duplicate acknowledgements remain recoverable. HTTP has 64 connection slots, a five-second absolute connection lifetime, a 2 MiB body limit and one state-processing job at a time. These are caps, not measured throughput or a guarantee against starvation under attack.

An offline recipient leaves delivery pending. The sender can keep composing until its bounded queue fills; further sends are then refused without dropping messages. Adding a member waits for the active delivery queue. Removal can proceed while messages or earlier membership commits are pending. Pending Events from continuing members are encrypted again for the new epoch with their original Event IDs, so recipients can suppress duplicates. A removed local member records its unsent Event IDs as undelivered_events and keeps its local history.

Up to 16 removal commits may queue behind an active membership batch. Older commits are delivered before subsequent commits and re-encrypted messages. The required recipients of an active commit are its historical members intersected with the current roster, excluding its sender. A removed member's receipt is not required, even when its Welcome was never acknowledged. Control packets have an additional 256 KiB allowance; all active and queued packets together remain bounded at 1280 KiB. Hitting a bound refuses further work.

The client retains message secrets for 17 past MLS epochs to finish bounded in-flight delivery. Only current members' older packets may be accepted. This retention increases exposure if local state is compromised; it is not a claim of immediate erasure or application-wide forward secrecy. Existing members must process missed commits in order. This small-cohort profile has no third-party relay archive or automatic pre-join history sharing. The sender's persisted queue is the recovery source until delivery; afterward each recipient retains its own bounded local Event history. If the only pending copy is lost, recovery fails. Removed members keep old plaintext but cannot send or read subsequent messages under the new epoch.

Public-trial readiness still requires the deployment gate in ../../docs/PRIVATE_THREADS_DEPLOYMENT_GATE.md; this document is not that approval.