#[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
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
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
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
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
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
derived: StringThe address the leaf’s own signature key derives to, or a rendering of why no address could be derived from it.
context: LeafSourceWhich 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.
SenderIdentityMismatch
The MLS-authenticated sender of a decrypted message does not match the sender the transport layer attributed the message to.
Fields
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
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
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.
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.
Implementations§
Source§impl MlsError
impl MlsError
Sourcepub fn privacy_safe_reason(&self) -> &'static str
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’speer_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::foundandSelf::WelcomeGroupIdMismatch::embeddedare 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 isfrom_utf8_lossyover rawGroupContextbytes, so unlike the wire field it passes through neither charset validation norGroupId::MAX_LEN.
§Contract
Two properties are load-bearing and neither is decoration:
- The return type is
&'static str, so a caller cannot interpolate a value into it. Widening this toStringwould restore the leak by the front door. - 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 Error for MlsError
impl Error for MlsError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<StorageError> for MlsError
impl From<StorageError> for MlsError
Source§fn from(source: StorageError) -> Self
fn from(source: StorageError) -> Self
Auto Trait Implementations§
impl Freeze for MlsError
impl RefUnwindSafe for MlsError
impl Send for MlsError
impl Sync for MlsError
impl Unpin for MlsError
impl UnsafeUnpin for MlsError
impl UnwindSafe for MlsError
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Declassify for T
impl<T> Declassify for T
type Declassified = T
fn declassify(self) -> T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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