Documentation
¶
Overview ¶
Package e2ee defines the contract a candidate end-to-end encryption profile must satisfy, and the bindings that tie one ciphertext to one conversation.
It carries a concrete default candidate, but that candidate remains a protocol-freeze decision: its presence is implementation evidence, not owner ratification. The package invents no cipher, MAC, signature, or ratchet. It fixes what the candidate provides, what a ciphertext is bound to, what published material commits, how a candidate is refuted, and how session state survives a crash.
The last of those is why the interface is a pure state transition rather than a mutable session object. A session that advances in memory has two truths, the one in memory and the one on disk, and every crash decides which survives. Returning the next state alongside the result makes the caller commit both together, and lets this repository state the commit order instead of discovering it from whichever library is chosen.
The conformance harness can only refute. A suite that fails a check definitively lacks the property; a suite that passes every check has cleared a floor, not earned an approval. Selection still requires cryptographic review.
Index ¶
- Constants
- Variables
- func BindBundle(delegation identity.Delegation, bundle Bundle, now time.Time) error
- func BindBundleSet(delegation identity.Delegation, bundles []Bundle, committed string, ...) error
- func BundleDigest(bundle Bundle) (string, error)
- func BundleSigningBytes(bundle Bundle) ([]byte, error)
- func DeviceSessionID(deviceA, deviceB string) (string, error)
- func EncodeBundleJSON(bundle Bundle) ([]byte, error)
- func EncodeBundleSetJSON(bundles []Bundle) ([]byte, error)
- func EncodeFirstContactJSON(value FirstContact) ([]byte, error)
- func MatchesDescriptorDigest(committed string, bundles []Bundle) error
- func SetCanonicalBytes(bundles []Bundle) ([]byte, error)
- func SetDigest(bundles []Bundle) (string, error)
- func ValidateAlgorithmID(algorithm string) error
- func ValidateBundle(bundle Bundle, signed bool) error
- func ValidateFirstContact(value FirstContact) error
- func ValidateSet(bundles []Bundle) error
- func ValidateState(state State) error
- type Binding
- type Bundle
- type FirstContact
- type PlanInput
- type PlanResult
- type SetEquivocationError
- type SetSummary
- type State
- type Succession
- type Suite
- type Target
Constants ¶
const ( // FirstContactSchema identifies the public, transportable evidence needed // by a recipient device to accept an asynchronous first-contact session. FirstContactSchema = "tos.messaging.e2ee.first-contact.v1" // MaxInitialMessageBytes bounds suite-specific asynchronous handshake data. MaxInitialMessageBytes = 4 << 10 )
const ( // BundleSchema is the strict wire schema identifier. BundleSchema = "tos.messaging.prekey-bundle.v2" // MaxMaterialBytes bounds one published prekey bundle's material. MaxMaterialBytes = 4 << 10 // MaxBundleLifetimeSeconds bounds how long published material stays valid. // Published prekeys are consumed by senders the owner never hears from, so // material that never expires is material that can never be retired. MaxBundleLifetimeSeconds = 30 * 24 * 60 * 60 // MaxDevicesPerSet bounds the devices one endpoint publishes at once. MaxDevicesPerSet = 16 )
const ( // BundleSetSchema identifies the bounded JSON publication wrapper for all // device bundles committed by one descriptor. The wrapper is not itself // signed; SetCanonicalBytes is the descriptor-committed representation. BundleSetSchema = "tos.messaging.prekey-bundle-set.v2" // MaxBundleSetWireBytes bounds an object fetched before JSON decoding. It // leaves room for MaxDevicesPerSet bundles at their material bound without // permitting an unbounded discovery response. MaxBundleSetWireBytes = 128 << 10 )
const CommitOrder = "inbound: event then state; outbound: state then ciphertext"
CommitOrder documents the order in which a caller must make things durable.
The two directions commit in opposite orders, and each order is decided by what a crash between the two writes would cost.
Inbound: the event record first, then the session state. A crash between them leaves the event durable and the state behind. The same ciphertext opens again to the same next state, so nothing is lost. The other order consumes the message key and then loses the event, and no retry can recover it: the peer's copy no longer opens. Outbound: the session state first, then the ciphertext. A crash between them advances the state and loses the ciphertext, so the message is sealed again from the new state under a fresh key. The other order leaves a released ciphertext with the state rolled back, and the next seal reuses a message key and nonce, which is the one failure a ratchet cannot absorb.
The plaintext is queued before it is sealed, so re-sealing after a crash has something to seal.
const DefaultCandidateAlgorithmID = "tos.messaging.e2ee.x3dh-aes256gcm-dr.v2"
DefaultCandidateAlgorithmID names the concrete suite proposed for the M0 freeze. Its presence does not freeze the value: the owner still has to ratify the decision package before this identifier becomes a wire promise.
const MaxCiphertextOverheadBytes = 512
MaxCiphertextOverheadBytes bounds how much a suite may add to a plaintext. Every message pays this on every hop and every Relay stores it, so an unbounded expansion is a protocol cost, not an implementation detail.
const MaxSessionStateBytes = 256 << 10
MaxSessionStateBytes bounds one persisted session state. State is committed on the critical path of every message, so a suite whose state grows without limit would make every send slower than the last.
Variables ¶
var ( // ErrSetRollback reports an observed publication older than the accepted // freshness watermark. ErrSetRollback = errors.New("prekey device set rolled back") // ErrSetEquivocation reports different non-retirement content at the same // freshness watermark. The two digests are intentionally not tie-broken: // arrival order is not endpoint authority. ErrSetEquivocation = errors.New("prekey device set equivocated") )
var ( // ErrReplayed reports a ciphertext this state has already opened. ErrReplayed = errors.New("message was already opened") // ErrNotAuthentic reports a ciphertext that failed authentication, whether // because it was altered or because its binding does not match. ErrNotAuthentic = errors.New("message is not authentic for this binding") // ErrSessionExpired reports a session past its permitted lifetime. ErrSessionExpired = errors.New("session has expired") // ErrStateUnusable reports state a suite cannot interpret, including state // written by a version it does not understand. ErrStateUnusable = errors.New("session state is unusable") )
Errors a suite must return rather than inventing its own semantics. A caller distinguishes a replayed message from a corrupt one, and a rejected binding from either.
var AlgorithmPattern = regexp.MustCompile(`^tos\.messaging\.e2ee\.[a-z0-9-]{1,32}\.v[0-9]{1,3}$`)
AlgorithmPattern matches a frozen suite identifier. A suite that cannot name itself cannot be negotiated, deprecated, or upgraded.
Functions ¶
func BindBundle ¶
BindBundle admits published material only under the delegation that authorized the key which signed it.
A sender uses this material to start a session with someone who is offline and cannot object, so the check runs before the material is used, not after the first reply.
func BindBundleSet ¶
func BindBundleSet(delegation identity.Delegation, bundles []Bundle, committed string, now time.Time) error
BindBundleSet admits a published device set under one delegation.
It does in one call what a caller would otherwise have to remember to do for each device: the set is coherent, every bundle is signed by the delegated key, none outlives the delegation, and the whole set reproduces the digest the descriptor committed.
func BundleDigest ¶
BundleDigest identifies one published bundle.
func BundleSigningBytes ¶
BundleSigningBytes returns the exact preimage the endpoint key signs.
func DeviceSessionID ¶
DeviceSessionID derives the session identifier for one device pair.
It is deterministic and symmetric: both ends compute the same identifier without negotiating, because a session-identity handshake would be one more exchange that can disagree. One pair, one session, across conversations -- the ratchet inside it is what provides freshness, not session churn.
func EncodeBundleJSON ¶
EncodeBundleJSON returns the publishable bundle.
func EncodeBundleSetJSON ¶
EncodeBundleSetJSON returns the complete publishable device set. The input order is preserved on the wire; identity remains order independent because the descriptor commits SetCanonicalBytes, which sorts bundle digests.
func EncodeFirstContactJSON ¶
func EncodeFirstContactJSON(value FirstContact) ([]byte, error)
EncodeFirstContactJSON returns the strict public bootstrap wire value.
func MatchesDescriptorDigest ¶
MatchesDescriptorDigest checks a published device set against the prekey bundle digest a Messaging Contact Descriptor committed.
A sender resolves the descriptor from finalized identity and then fetches material from wherever it is served. This is what stops the two from disagreeing: material that does not reproduce the committed digest is not the material the endpoint published, whatever the server that returned it claims.
func SetCanonicalBytes ¶
SetCanonicalBytes returns the exact preimage committed by a descriptor's prekey bundle digest. It is exported so another implementation can consume the same positive vector without treating the JSON publication wrapper as the committed representation.
func SetDigest ¶
SetDigest is the value a Messaging Contact Descriptor commits as its prekey bundle digest.
It covers every device's bundle, so a descriptor cannot be paired with a device set the endpoint never published, and adding or removing a device changes the descriptor rather than happening silently underneath it. The order devices are listed in does not change the result.
func ValidateAlgorithmID ¶
ValidateAlgorithmID reports whether a suite identifier is well formed.
func ValidateBundle ¶
ValidateBundle enforces every structural rule.
func ValidateFirstContact ¶
func ValidateFirstContact(value FirstContact) error
ValidateFirstContact binds the independently signed sender prekey to the asserted sender and the exact directional session context.
func ValidateSet ¶
ValidateSet enforces that a published set is one endpoint's devices and nothing else.
A protocol core cannot rely on every caller remembering to check each bundle afterwards. A set that mixes Agents, networks, or suites would produce a digest a descriptor could commit, and the mixing would only be noticed by whoever happened to verify the bundles individually.
func ValidateState ¶
ValidateState enforces the bounds every persisted state must respect.
Types ¶
type Binding ¶
type Binding struct {
Network *nativev1.NetworkDomain
AlgorithmID string
ConversationID string
SenderAgentID string
SenderEndpointID string
SenderDeviceID string
RecipientAgentID string
RecipientEndpointID string
RecipientDeviceID string
}
Binding names exactly where one ciphertext belongs.
It is passed to Seal and Open as associated data, so a ciphertext that is lifted out of its conversation, replayed in the other direction, or presented under a different suite fails to open rather than decrypting into the wrong context. None of this is confidentiality: it is the reason a message means what its position says it means.
type Bundle ¶
type Bundle struct {
Network *nativev1.NetworkDomain
AgentID string
EndpointID string
DeviceID string
AlgorithmID string
Material []byte
IssuedAtUnix uint64
ExpiresAtUnix uint64
EndpointSignature []byte
}
Bundle is one device's published prekey material.
It is signed by the delegated Messaging Endpoint key, never by the Agent controller key, and it means nothing until the delegation behind that key is resolved from finalized TOS state.
func DecodeBundleJSON ¶
DecodeBundleJSON rejects unknown fields, trailing data, and malformed bundles.
func DecodeBundleSetJSON ¶
DecodeBundleSetJSON strictly decodes a bounded publication wrapper and enforces whole-set coherence. Individual bundles retain their own strict schema and signature-shape validation.
func SignBundle ¶
func SignBundle(bundle Bundle, endpointKey ed25519.PrivateKey) (Bundle, error)
SignBundle signs published material with the delegated endpoint key.
func SignBundleWith ¶
SignBundleWith signs through a narrow crypto.Signer boundary. An endpoint may keep its online identity key in an HSM or another isolated process; the prekey publisher needs signatures, not possession of the key bytes.
The returned signature is verified before it leaves this function. A remote signer that selected the wrong key or returned malformed output therefore cannot create a publication that only fails after the descriptor changes.
type FirstContact ¶
type FirstContact struct {
Binding Binding
SenderBundle Bundle
RecipientBundleDigest string
Initial []byte
}
FirstContact contains no secret material. The recipient independently verifies SenderBundle under finalized Endpoint authority and selects the exact local private prekey named by RecipientBundleDigest before accepting the session. Binding is the authority-bearing context used as AEAD AAD.
func DecodeFirstContactJSON ¶
func DecodeFirstContactJSON(raw []byte) (FirstContact, error)
DecodeFirstContactJSON rejects ambiguous encodings and revalidates every digest and identity relationship before returning bootstrap evidence.
type PlanInput ¶
type PlanInput struct {
// SenderDeviceID is this device.
SenderDeviceID string
// SenderSet is the sender's own current set, for self-fan-out.
SenderSet []Bundle
// RecipientSet is the recipient's current set.
RecipientSet []Bundle
// Now bounds bundle expiry for bootstraps.
Now time.Time
// SessionExists reports whether a session identifier is established.
SessionExists func(sessionID string) bool
}
Plan computes the per-device fan-out for one logical event.
One event, one identifier, many sealed copies: every live device of the recipient gets one, and every other device of the sender gets one, so the sender's own devices agree about what was said. The event's identity is its content, so the copies are the same event, not siblings.
sessionExists answers whether a session is already established; expired bundles are skipped for bootstrap but do not close established sessions, because a bundle only ever bootstraps -- the ratchet is the ongoing key. A recipient device whose bundle has expired and with whom no session exists is simply unreachable until its owner rotates, and the plan says so rather than guessing.
type PlanResult ¶
type PlanResult struct {
Recipients []Target
// SelfCopies are the sender's other devices.
SelfCopies []Target
// Unreachable are recipient devices with no session and no live bundle.
Unreachable []string
}
Plan output: recipient targets, then self targets, both sorted by device.
func FanOut ¶
func FanOut(input PlanInput) (PlanResult, error)
FanOut plans the sealed copies for one event.
type SetEquivocationError ¶
type SetEquivocationError struct {
CurrentDigest string
CandidateDigest string
IssuedAtUnix uint64
}
SetEquivocationError identifies the two conflicting commitments. The caller already holds the signed candidate set and can export this summary alongside it; errors.Is(err, ErrSetEquivocation) classifies the refusal.
func (*SetEquivocationError) Error ¶
func (e *SetEquivocationError) Error() string
func (*SetEquivocationError) Unwrap ¶
func (e *SetEquivocationError) Unwrap() error
Unwrap supports errors.Is without discarding the conflicting digests.
type SetSummary ¶
type SetSummary struct {
// Digest is the set's identity, as committed in the descriptor.
Digest string
// EndpointID is whose devices these are.
EndpointID string
// DeviceIDs is the sorted device list.
DeviceIDs []string
// BundleDigests is the sorted per-bundle digest list. It is what lets a
// pure retirement be recognised: a successor whose bundles are a subset of
// the current ones removed devices and changed nothing else.
BundleDigests []string
// NewestIssuedAtUnix is the freshest issuance in the set. A successor that
// is not a pure retirement must be strictly fresher, which is what stops
// an old set being replayed as a new one.
NewestIssuedAtUnix uint64
}
SetSummary is what a receiver remembers about a peer's device set.
It is the anchor for two protections a single set cannot provide. Rollback: a peer's directory entry can be replayed by whoever can reach the DHT, so the receiver keeps the newest set it has accepted and refuses regressions. Revocation: a device removed from the set must stay removed, so the summary is judged against tombstones the receiver also keeps.
func Summarize ¶
func Summarize(bundles []Bundle) (SetSummary, error)
Summarize reduces a valid set to what succession is judged on.
type State ¶
type State []byte
State is one session's complete persisted state.
It is opaque, and it is everything the suite needs to continue: keys, ratchet position, skipped-message keys, and replay bookkeeping. A state that omitted replay bookkeeping would let a restart re-accept a message the session had already opened.
type Succession ¶
type Succession struct {
// Accepted is the summary to record.
Accepted SetSummary
// Removed are the devices this succession revoked. Their sessions are to
// be closed, and their identifiers never return.
Removed []string
}
Succession is the outcome of judging a successor set.
func Succeed ¶
func Succeed(current SetSummary, tombstones map[string]struct{}, next []Bundle) (Succession, error)
Succeed judges whether a set may replace the one on record.
The rules are few and each has one reason:
- a device on the tombstone list never returns. Removal is revocation, and revocation with an undo is a suggestion. A device that legitimately comes back generates a fresh key, which is a fresh identifier;
- a pure retirement -- every bundle already in the current set, some devices gone -- is accepted at the same freshness, because removing a device should not require re-issuing everyone else's material;
- anything else must be strictly fresher than what is on record. Equal freshness with different content is two sets claiming the same moment, and the receiver has no way to order them except by whoever spoke last, which is exactly the authority a replayed directory entry would have.
current may be the zero SetSummary for a peer never seen before; tombstones still apply, because a first sight that includes an already-revoked device is a first sight of a forgery.
type Suite ¶
type Suite interface {
// AlgorithmID returns the frozen suite identifier. It must be stable for
// the lifetime of the value.
AlgorithmID() string
// NewPrekeyMaterial produces the public material an endpoint publishes and
// the private state that answers it.
NewPrekeyMaterial() (public []byte, private []byte, err error)
// Initiate starts a session using this device's private prekey material and
// the peer's published material. Requiring both is the possession proof
// behind the identities in the binding; published material alone would let
// anyone initiate while claiming to be any delegated endpoint.
Initiate(private []byte, peerPublic []byte, binding []byte) (State, []byte, error)
// Accept completes a session from an initial message and the initiator's
// independently fetched, endpoint-signed published material.
Accept(private []byte, peerPublic []byte, initial []byte, binding []byte) (State, error)
// Seal encrypts under a binding and returns the state that must be durable
// before the ciphertext is released.
Seal(state State, plaintext []byte, binding []byte) (ciphertext []byte, next State, err error)
// Open decrypts under a binding and returns the state that must be durable
// before the plaintext is acted on.
Open(state State, ciphertext []byte, binding []byte) (plaintext []byte, next State, err error)
// KeyMaterial returns the part of a state an attacker obtains when they
// take the device: keys, without replay bookkeeping.
//
// It exists so a compromise check cannot be satisfied by retaining a list
// of what was already seen. Whether an implementation kept such a list has
// no bearing on whether past traffic is readable, and a suite whose
// persisted state was accepted as the attacker's view could appear to
// protect a backlog it does not protect.
//
// Production code has no reason to call this.
KeyMaterial(state State) (State, error)
}
Suite is a candidate cryptographic profile.
Establishment is asynchronous by construction: an initiator starts from published material alone, because the recipient of a first message is usually offline. A suite that requires both parties online cannot serve this Messenger.
func NewDefaultSuite ¶
func NewDefaultSuite() Suite
NewDefaultSuite returns the route-independent one-to-one suite proposed by the decision package. The suite uses the operating system's cryptographic random source and keeps all mutable protocol state in the State values returned by its methods.
type Target ¶
type Target struct {
DeviceID string
SessionID string
// Bootstrap reports that no session exists yet and one has to be built
// from the device's published bundle.
Bootstrap bool
// BundleDigest names the bundle to bootstrap from, when Bootstrap is set.
BundleDigest string
}
Target is one sealed copy the fan-out owes.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package conformance refutes candidate end-to-end encryption suites.
|
Package conformance refutes candidate end-to-end encryption suites. |