Skip to main content

KeyPackagePayload

Struct KeyPackagePayload 

Source
pub struct KeyPackagePayload {
    pub user_id: String,
    pub key_package_data: Vec<u8>,
    pub remaining_lifetime_ms: u64,
    pub timestamp_ms: u64,
    pub session_reset: bool,
    pub wire_versions: Vec<u8>,
    pub env_versions: Vec<u8>,
    pub rich_versions: Vec<u8>,
    pub data_versions: Vec<u8>,
    pub ctrl_versions: Vec<u8>,
    pub nostr_pubkey: Option<String>,
}
Expand description

Payload for key package exchange.

Every field except user_id and key_package_data carries #[serde(default)], so a payload written by an older peer that has never heard of a field decodes cleanly with that field empty. Empty selects the floor, by rule and never by error, which is what makes a mixed fleet safe.

Fields§

§user_id: String

User ID of the key package owner.

§key_package_data: Vec<u8>

Raw key package data.

A bare key package, not one wrapped in an MLS message. Both forms are legal MLS and only one of them is this protocol’s wire, so an implementation whose convenience API returns the wrapped form has to unwrap it before encoding.

§remaining_lifetime_ms: u64

Remaining valid lifetime in milliseconds (relative, not absolute). Receiver applies this to their local clock, avoiding clock skew issues.

§timestamp_ms: u64

Legacy absolute timestamp field, ignored on receive, kept for backward compatibility with old nodes that may still send it.

§session_reset: bool

When true, the sender has reset their MLS session state and the receiver should discard any existing session for this peer before establishing a new one.

Two senders set it. Post-unblock convergence: when Alice unblocks Bob, Alice deletes her MLS session and sends a fresh key package with this flag, so Bob deletes his now-orphaned session and both converge on one fresh group. And a driven session rekey, which is how post-compromise security reaches a peer that never commits: the rekey arrives as this flag on a key package, never as an unsolicited Welcome.

A receiver that treats a reset as an ordinary key package refresh keeps a session the sender has already discarded, and every later frame from it decrypts to nothing.

§wire_versions: Vec<u8>

Wire-format versions the sender can decode (e.g. [1] for binary v1). Absent on legacy nodes (#[serde(default)] gives empty, meaning JSON only), so an old peer is never sent a binary frame it cannot parse.

Trust boundary: this rides in the plaintext payload alongside the signed MLS key_package_data, not inside the signature, so it is not cryptographically bound to the sender. An attacker on the pre-session bootstrap could strip it (a harmless JSON downgrade) or forge [1] onto a legacy peer (making us emit binary that peer drops, a targeted delivery denial of service). This grants no new capability: such an attacker already controls key package delivery and could deny service outright. The negotiation is a performance optimization, never a security control.

§env_versions: Vec<u8>

MLS envelope formats the sender can parse (e.g. [1] for the compact envelope, MLS_ENVELOPE_COMPACT_V1). Absent on legacy nodes (#[serde(default)] gives empty, meaning the legacy JSON envelope only), so an old peer is never sent an envelope it cannot parse.

Distinct from wire_versions: that one is hop-local (which frames the peer decodes), this one is end-to-end (which prefixes::ENCRYPTED payload encodings the recipient parses after any number of relay hops).

Trust boundary: identical to wire_versions above, a plaintext field and not signature-bound. Stripping it downgrades to the JSON envelope (harmless); forging it onto a legacy peer makes us emit envelopes that peer rejects with a message_decryption_failed event (a targeted delivery denial of service an attacker in that position already has). A performance optimization, never a security control.

§rich_versions: Vec<u8>

Sealed rich-payload versions the sender can parse (e.g. [1] for RICH_PAYLOAD_V1). Absent on legacy nodes (#[serde(default)] gives empty, meaning plain text only), so an old peer is never sent a __RICH_V1__ body it would surface as raw JSON text.

End-to-end like env_versions (what the recipient parses inside the decrypted MLS plaintext), not hop-local like wire_versions.

Trust boundary: identical to the two fields above, a plaintext field and not signature-bound. Stripping it downgrades to plain text with the rich extras dropped (harmless); forging it onto a legacy peer makes us seal bodies that peer renders as JSON text (a nuisance an attacker in that position could match by corrupting delivery outright). A feature negotiation, never a security control.

§data_versions: Vec<u8>

Replicated-document sync versions the sender speaks (e.g. [1] for DATA_SYNC_V1). Absent on legacy nodes and on installs with the data layer switched off (#[serde(default)] gives empty), and toward a peer that advertises nothing here no __DATA_V1__ frame is ever sent.

End-to-end like env_versions and rich_versions: it says what the recipient parses inside the decrypted MLS plaintext, not what a directly connected neighbour decodes on the wire.

The version doubles as the engine’s encoding generation. The CRDT engine promises that new code reads old encodings and says nothing about the reverse, so a future encoding change is a new version byte here and a mixed fleet declines what it cannot read instead of discovering it at import.

Trust boundary: identical to the capability lists above, a plaintext field and not signature-bound. Stripping it stops documents replicating with that peer (they stay editable locally and converge whenever a genuine advertisement arrives); forging it onto a peer that cannot parse the frames makes us send bodies they consume and drop. Neither grants an attacker anything they could not achieve by dropping the packets outright. A feature negotiation, never a security control.

§ctrl_versions: Vec<u8>

Control-frame signing versions the sender verifies (e.g. [2] for CTRL_SIGN_V2, the payload that binds the frame’s timestamp). Absent on legacy nodes (#[serde(default)] gives empty, meaning the v1 payload only), so a peer that has never heard of the freshness-bound domain is never sent a signature it would read as invalid.

Unlike every capability list above, this one says what the sender accepts, not what it emits. It has to: a signature is produced once and verified by the far end, so the choice of domain belongs to whoever is going to check it.

Trust boundary: this is a plaintext field like the others, and an attacker who strips it makes us sign the older payload toward a peer that would have accepted the newer one. That downgrade is real and it is why stripping it is not the end of the story: a receiver that has once verified a v2 signature from a peer records that durably and refuses that peer’s v1 control frames from then on, so the strip works only until the first genuine v2 frame arrives and never afterwards. The field is what makes the first one possible; the record is what makes it stick.

§nostr_pubkey: Option<String>

This install’s Nostr public key (x-only, 64-char lowercase hex), so a peer can seal Nostr gift wraps to a key only this install holds.

None on legacy nodes and on installs with Nostr disabled. A peer without it seals to our publicly computable key instead, deliverable either way, but readable by anyone who guesses our user id, so the difference is real privacy rather than a mere optimization.

Trust boundary: unlike the capability lists above, this one is only honoured from a signed key package. All four ride in the same plaintext payload, but this field is consumed as a destination key, not as a feature hint, so the distinction matters: a wrong capability costs a fallback, whereas a wrong key here means envelope metadata is sealed to whoever supplied it and is then readable off a public relay, passively, for as long as the value stands.

The canonical signing payload covers the whole prefixes::KEY_PACKAGE body under the sender’s Ed25519 signature, which the gate verifies against the key their address derives from, so on this prefix an unsigned frame does not reach dispatch at all. The receiving path still consumes this field only when the gate reports the frame was actually signed, which costs nothing: a key package exists only once MLS is initialized, and the sender signs unconditionally in that state, so every genuine package carrying this field is signed.

Stripping it is still possible for a network attacker and downgrades us to the bootstrap key, which is a privacy downgrade, not a disclosure to the attacker, and one they could equally achieve by dropping the packet.

Trait Implementations§

Source§

impl Clone for KeyPackagePayload

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for KeyPackagePayload

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for KeyPackagePayload

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for KeyPackagePayload

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more