Skip to main content

MlsError

Enum MlsError 

Source
#[non_exhaustive]
pub enum MlsError {
Show 37 variants Storage(StorageError), CryptoGeneration(String), CredentialCreation(String), KeyPackageCreation(String), InvalidKeyPackage(String), GroupCreation(String), GroupNotFound(String), AddMember(String), RemoveMember(String), Encryption(String), Decryption(String), SessionDesync(String), WelcomeProcessing(String), Serialization(String), Deserialization(String), NotInitialized, NoKeyPackage(String), SessionNotFound(String), CommitNotAuthorized { committer: String, added: Vec<String>, removed: Vec<String>, }, InvalidMessage(String), UserNotInGroup(String), OpenMls(String), Signing(String), VerificationFailed(String), InvalidPublicKey(String), IdentityAddressMismatch { expected: String, derived: String, }, InvalidGroupId(String), InvalidUserId(String), CredentialIdentityMismatch { expected: String, found: String, }, KeyPackageAddressMismatch { claimed: String, derived: String, }, LeafAddressMismatch { claimed: String, derived: String, context: LeafSource, }, UnsupportedSender { detail: String, }, SenderIdentityMismatch { claimed: String, authenticated: String, }, SessionIdentityMismatch { expected: String, found: String, }, WelcomeIdentityMismatch { expected: String, found: String, }, ReservedSessionNamespace { group_id: String, }, WelcomeGroupIdMismatch { expected: String, embedded: String, },
}
Expand description

Errors that can occur during MLS operations.

Variants (Non-exhaustive)§

This enum is marked as non-exhaustive
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

Storage(StorageError)

Storage operation failed.

§

CryptoGeneration(String)

Failed to generate cryptographic material.

§

CredentialCreation(String)

Failed to create a credential.

§

KeyPackageCreation(String)

Failed to create a key package.

§

InvalidKeyPackage(String)

Invalid key package received.

§

GroupCreation(String)

Failed to create a group.

§

GroupNotFound(String)

Group not found.

§

AddMember(String)

Failed to add member to group.

§

RemoveMember(String)

Failed to remove member from group.

§

Encryption(String)

Encryption failed.

§

Decryption(String)

Decryption failed.

§

SessionDesync(String)

Decryption failed because the local session is out of sync with the sender’s epoch (the two sides disagree on the MLS epoch), as opposed to a ciphertext that failed authentication. This is a recoverable failure: tearing down and re-establishing the 1:1 session restores the channel. It is deliberately distinct from MlsError::Decryption so the protocol layer can withhold the delivery ACK and trigger a re-key instead of silently dropping the message. Message-specific ratchet failures (a discarded past generation) and AEAD/corrupt failures are NOT classified here — re-keying would not help, and routing them into a re-key would add a second, unbounded storm vector on top of the one below.

This classification does not mean the frame was authentic. The epoch is read during framing validation, before any AEAD, sender-data or signature check, so a frame forged with no key material at all lands here (manager.rs::test_forged_frame_reaches_session_desync_without_any_key_material). Callers must treat it as an unauthenticated hint and keep whatever they hang off it bounded and non-destructive; see the SECURITY block on OfflineProtocol::schedule_session_rekey in the offline-protocol crate.

§

WelcomeProcessing(String)

Failed to process Welcome message.

§

Serialization(String)

Failed to serialize data.

§

Deserialization(String)

Failed to deserialize data.

§

NotInitialized

MLS manager not initialized.

§

NoKeyPackage(String)

No key package available for the user.

§

SessionNotFound(String)

Session not found for 1:1 messaging.

§

CommitNotAuthorized

A membership commit was refused before merging because the local admin policy did not authorize its committer or proposal senders.

Only produced when commit enforcement is explicitly enabled (GroupConfig::enforce_admin_commits, default off) — see GroupManager::authorize_membership_commit for the fail-open rules that keep an incomplete local admin view from rejecting a legitimate commit and forking the group. Permanent: the same commit can never become authorized, so callers must not buffer and retry it.

Fields

§committer: String

The MLS-authenticated committer whose commit was refused.

§added: Vec<String>

User ids the refused commit would have added, sorted.

§removed: Vec<String>

User ids the refused commit would have removed, sorted.

§

InvalidMessage(String)

Invalid message format.

§

UserNotInGroup(String)

User not found in group.

§

OpenMls(String)

OpenMLS library error.

§

Signing(String)

Signing operation failed.

§

VerificationFailed(String)

Signature verification failed.

§

InvalidPublicKey(String)

Invalid public key format.

§

IdentityAddressMismatch

The stored identity key does not derive to the address this manager was constructed for.

Under self-certifying addressing the address is the identity key’s hash, so these two can only disagree if the wrong storage namespace was opened for this profile, or a stored keypair was replaced. Continuing would build a credential claiming an address this device cannot prove it owns — every peer would reject it at the derivation check, so the failure is raised here where it is still legible.

Only reachable for address-shaped ids: a manager constructed with a legacy/nickname id has no derivation to check against.

Fields

§expected: String

The address this manager was constructed for.

§derived: String

The address the stored identity key actually derives to.

§

InvalidGroupId(String)

Group id failed storage-key validation (empty, oversized, or containing path-traversal / storage-hostile characters).

§

InvalidUserId(String)

User id failed storage-key validation (empty, or containing path-traversal / storage-hostile characters).

§

CredentialIdentityMismatch

A key package’s embedded credential identity does not match the user id it was claimed to belong to.

Fields

§expected: String

The user id the key package was claimed to belong to.

§found: String

The identity actually embedded in the key package credential.

§

KeyPackageAddressMismatch

A key package’s leaf signature key does not derive to the address it claims to belong to.

Distinct from MlsError::CredentialIdentityMismatch, and it has to be: credentials here are MLS basic credentials, whose content is a bare self-asserted identity string. Anyone can generate a signature keypair and stamp an address on it, so the identity check catches only a careless substitution. This one recomputes the address from the key, and so catches a deliberate one.

RFC 9420 leaves credential validation to the application’s Authentication Service. For this SDK that service is a pure derivation: an address is bech32m(0x01 ‖ SHA-256(signature_key)[..20]), so the claim carries its own proof and needs no prior contact to check. This is where that verdict is enforced at key-package use time.

Fields

§claimed: String

The address the key package was claimed to belong to.

§derived: String

The address its leaf signature key actually derives to.

§

LeafAddressMismatch

A leaf node in a ratchet tree carries a credential its own signature key does not derive to.

The tree-side sibling of MlsError::KeyPackageAddressMismatch. That one fires on a key package this device supplied — imported for a contact, read back from the contact cache, or handed to add_group_member. This one fires on a leaf that entered the tree some other way: a Welcome’s ratchet tree, or an Add/Update another member committed. The two are kept separate so the error text says which gate refused, because the recovery differs — a bad key package means re-exchange, a bad leaf means someone in the group is forging identities.

RFC 9420 §5.3.1 puts this check on the application: the Authentication Service must verify “that the credential’s presented identifiers are correctly associated with the signature_key field in the member’s LeafNode”, and §7.3 applies that “when a client validates a ratchet tree, e.g., when joining a group or after processing a Commit”. OpenMLS does not do it for you — its own external-commit validation says so in as many words (“This MUST be checked by the application”), and StagedCommit::credentials_to_verify exists precisely to hand the application the credentials it must judge.

Without this, SEC-M1 (MlsError::SenderIdentityMismatch) is checking a wire-claimed sender against a self-asserted string: a member who commits a leaf whose credential names someone else is then attributed as that someone else, which on the __GROUP_MSG__ data-plane path needs no signature from anyone.

Fields

§claimed: String

The identity the leaf’s credential asserts.

§derived: String

The address the leaf’s own signature key derives to, or a rendering of why no address could be derived from it.

§context: LeafSource

Which gate refused, so a report names the path rather than only the verdict (see LeafSource).

§

UnsupportedSender

A message, or a commit, was authenticated to a sender that is not a member of the group.

MLS admits three such senders: an external joiner committing itself in (NewMemberCommit), a would-be member proposing its own Add (NewMemberProposal), and a party authorized by the group’s ExternalSenders extension. This SDK issues none of them and configures no external senders, so any of the three arriving is either a peer running something else or an attacker.

They are refused rather than ignored because each one is a leaf, or a signing key, that MlsError::LeafAddressMismatch cannot judge the usual way: an external joiner’s leaf has no prior entry in the tree to compare against, and an ExternalSenders credential is not a leaf at all. Refusing keeps the tree to identities that entered through a gate this SDK actually operates.

One further condition shares this variant because it shares the disposition exactly — permanent, unattributable, never buffered: a sender index that resolves to no leaf at all. That is a tree-integrity fault rather than an unsupported role, and it cannot happen for a message OpenMLS just authenticated against that leaf, so it is defensive; its detail is prefixed tree integrity: to keep the two readable apart in a log.

Contract for anything added here. Three separate dispositions key off the bare variant — process_commit_core’s is_permanent, is_media_security_rejection, and the __MLS_ENC__ intercept in handle_encrypted_message — so a new condition mapped to this variant silently inherits permanent, un-ACKed, and reported as a security warning. Only map conditions that genuinely share all three: the frame can never become processable, the sender must learn nothing from an ACK, and no honest peer produces it. Anything recoverable needs its own variant.

Fields

§detail: String

Which unsupported sender shape arrived, and where.

§

SenderIdentityMismatch

The MLS-authenticated sender of a decrypted message does not match the sender the transport layer attributed the message to.

Fields

§claimed: String

The sender the wire envelope claimed.

§authenticated: String

The identity authenticated by the MLS credential.

§

SessionIdentityMismatch

A 1:1 envelope’s group_id does not name the session slot shared with the sender the transport layer attributed it to.

This is the failure-path counterpart to MlsError::SenderIdentityMismatch, and the two are not interchangeable: SEC-M1 binds the wire sender to the MLS credential, but that credential only exists once process_message succeeds. Every pre-authentication verdict — most importantly the framing checks OpenMLS runs before any AEAD (group id, then epoch, yielding MlsError::SessionDesync) — is reached with the wire sender still entirely unverified. Since a 1:1 slot id is session:<a>:<b> over two public user ids, anything keyed off such a verdict would otherwise be reachable by a stranger naming any session and any sender, with no key material at all. Requiring the envelope’s slot to be the one shared with the claimed sender is the only binding that covers those paths, so it runs before the group is loaded.

Fields

§expected: String

The session slot shared with the claimed sender.

§found: String

The slot the envelope’s group_id actually named.

§

WelcomeIdentityMismatch

A 1:1-session Welcome’s group_id — the session storage slot it would install into — does not match the slot for the (local user, authenticated inviter) pair. Rejecting this stops an authenticated peer from installing or overwriting a third party’s 1:1 session slot, which would hijack the victim’s session so their outbound messages encrypt to the attacker’s group. This guards the session-Welcome (join_session) half of SEC-M6; the group-Welcome half is guarded by MlsError::ReservedSessionNamespace.

Fields

§expected: String

The only session slot the authenticated inviter may write.

§found: String

The slot the Welcome’s group_id actually named.

§

ReservedSessionNamespace

A group Welcome named a group_id in the reserved session: namespace. That namespace is owned exclusively by identity-bound 1:1 sessions (installed only via join_session, guarded by MlsError::WelcomeIdentityMismatch). A group Welcome carries an attacker-controllable group_id with no (self, inviter) binding and installs into the same storage/OpenMLS keyspace, so allowing one to name a session slot would let an authenticated peer seed or overwrite a third party’s 1:1 session — the identical hijack SEC-M6 blocks on the session-Welcome path, reached through the group path. This is the group-Welcome half of that binding.

Fields

§group_id: String

The reserved-namespace slot the group Welcome tried to install into.

§

WelcomeGroupIdMismatch

A Welcome’s embedded MLS group id (the id OpenMLS actually persists the joined group under) does not match the wire group_id the caller authenticated and keys storage by. The embedded id lives in the Welcome’s GroupContext and is chosen freely by the inviter at group creation (new_with_group_id); every SEC-M5/M6 binding validates only the wire field. Left unchecked, into_group would persist the group under the attacker-chosen embedded id — seeding or overwriting an arbitrary session/group slot the wire-id checks never inspected, the same hijack SEC-M6 blocks, reached through the identifier it does not cover. Rejecting binds the two before any state is written.

Fields

§expected: String

The wire group_id the caller authenticated and keys storage by.

§embedded: String

The group id embedded in the Welcome’s GroupContext (attacker-chosen).

Implementations§

Source§

impl MlsError

Source

pub fn privacy_safe_reason(&self) -> &'static str

A fixed, identifier-free classification of this error, safe to place in a field that reaches a TelemetrySink.

§Why this exists

The protocol crate’s telemetry scrubber hashes the named actor fields of an event (peer_id, group_id, sender, …) and ships free-text reason/detail fields verbatim — deliberately, since scrubbing payload is a separate concern from scrubbing identity. Rendering an MlsError into one of those fields therefore hands a sink running scrub_ids: true exactly what the setting exists to withhold, and it does so by two routes:

  • Identifiers this device holds. A 1:1 slot id is session:<a>:<b>, so a single rendered slot carries two addresses — neither of which is the event’s peer_id, so neither is hashed, and one of which may be a third party who is not a participant in the exchange at all. Storage keys, group ids and user ids render the same way.
  • Text the wire chose. Self::WelcomeIdentityMismatch::found and Self::WelcomeGroupIdMismatch::embedded are attacker-controlled by construction — bounding what they may contain is the entire premise of the checks that raise them. The embedded id is the worse of the two: it is from_utf8_lossy over raw GroupContext bytes, so unlike the wire field it passes through neither charset validation nor GroupId::MAX_LEN.
§Contract

Two properties are load-bearing and neither is decoration:

  1. The return type is &'static str, so a caller cannot interpolate a value into it. Widening this to String would restore the leak by the front door.
  2. The match below is exhaustive, which is legal despite #[non_exhaustive] only because it lives in the defining crate. That is the point: a new variant added above fails to compile here, in the same file, forcing the privacy decision at the moment the variant is written. Do not add a _ => arm — a wildcard turns this from a guarantee back into the per-site opt-in it replaced.

Callers keep the full error where it is useful and safe: the warn! or error! at the refusal site, which goes to the device log rather than to a sink.

Trait Implementations§

Source§

impl Debug for MlsError

Source§

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

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

impl Display for MlsError

Source§

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

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

impl Error for MlsError

Source§

fn source(&self) -> Option<&(dyn Error + 'static)>

Returns the lower-level source of this error, if any. Read more
1.0.0 · Source§

fn description(&self) -> &str

👎Deprecated since 1.42.0:

use the Display impl or to_string()

1.0.0 · Source§

fn cause(&self) -> Option<&dyn Error>

👎Deprecated since 1.33.0:

replaced by Error::source, which can support downcasting

Source§

fn provide<'a>(&'a self, request: &mut Request<'a>)

🔬This is a nightly-only experimental API. (error_generic_member_access)
Provides type-based access to context intended for error reports. Read more
Source§

impl From<SealedError> for MlsError

Carries a sealed-layer failure into this crate’s error type.

Each arm maps to the variant the moved code used to raise, and passes the inner string through rather than the rendered Display, so the text a caller sees is byte-identical to what it was before the move. Two test suites assert on substrings of it.

SealedError is deliberately not #[non_exhaustive], so this match is exhaustive and a new variant over there is a compile error here until someone decides what it means. A wildcard arm would render it as some other error’s text instead.

Source§

fn from(err: SealedError) -> Self

Converts to this type from the input type.
Source§

impl From<StorageError> for MlsError

Source§

fn from(source: StorageError) -> Self

Converts to this type from the input type.

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> Classify for T

Source§

type Classified = T

Source§

fn classify(self) -> T

Source§

impl<T> Declassify for T

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> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

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

Source§

type Error = Infallible

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

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<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

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