- Device identity — the Wire client ID (user UUID + device hex ID + domain)
- User identity — the Wire handle and display name
E2EI requires an ACME server that supports the custom
wire-dpop-01 and wire-oidc-01 challenge types. The only production-ready implementation is Wire’s fork of step-ca. A standard ACME server such as Let’s Encrypt will not work.Enrollment flow overview
Enrollment is a multi-step protocol that involves three parties: the Wire client, the ACME server (step-ca), and the Wire backend server plus an OIDC identity provider.Bootstrap ACME session
The client fetches the ACME directory to discover endpoint URLs, requests a nonce, and creates an ACME account. No user-specific data is committed yet.
Create an order with Wire identifiers
The client posts a new order to the ACME server containing two identifiers: a
wireapp-device identifier (client ID, handle, display name, domain) and a wireapp-user identifier (handle, display name, domain). The server responds with two authorization URLs.Fetch authorizations and challenges
The client posts to each authorization URL. The server returns one challenge per authorization:
- A
wire-dpop-01challenge for the device identifier - A
wire-oidc-01challenge for the user identifier
Complete the DPoP challenge
The client fetches a nonce from the Wire server (
GET /clients/{id}/nonce), constructs a DPoP token that binds the ACME challenge token to the client keypair, and trades it at the Wire server’s access-token endpoint (POST /clients/{id}/access-token). The resulting access token is submitted to the ACME server to satisfy the wire-dpop-01 challenge.Complete the OIDC challenge
The client initiates an OIDC Authorization Code + PKCE flow with the user’s identity provider. The IdP embeds
keyauth and acme_aud claims in the returned ID token. The client extracts the ID token and submits it to the ACME server to satisfy the wire-oidc-01 challenge.Finalize the order with a CSR
Once both challenges are validated, the client polls the order until it reaches
ready state, then submits a CSR. The ACME server issues the certificate chain.Challenge types
wire-dpop-01
Device identity challenge. Proves that the enrolling device controls the Wire client keypair and that the client ID registered with the Wire backend matches the identifier in the order.The Wire server acts as an intermediary: it verifies the DPoP token and signs an access token with a key known to the ACME server. The ACME server then verifies both the outer access token and the inner DPoP proof.
wire-oidc-01
User identity challenge. Proves that the enrolling user authenticated with the organization’s identity provider and that the handle and display name in the IdP match the identifiers in the order.The OIDC flow uses PKCE and requires the IdP to copy
keyauth and acme_aud claims into the returned ID token. The ACME server verifies the ID token signature and validates those claims.DPoP tokens
A DPoP (Demonstrating Proof of Possession) token is a short-lived JWT that proves ownership of the ACME account keypair at the time of the request. The DPoP token for thewire-dpop-01 challenge includes:
| Claim | Description |
|---|---|
sub | Wire client URI (wireapp://<user>!<device>@<domain>) |
aud | ACME challenge URL |
nonce | Nonce from the Wire server |
chal | ACME challenge token |
handle | Wire user handle URI |
htu | Wire access-token endpoint URL |
htm | HTTP method (POST) |
new_dpop_token method on RustyE2eIdentity.
OIDC tokens
The OIDC ID token is issued by the user’s identity provider after successful authentication. For thewire-oidc-01 challenge it must contain two additional claims beyond the standard OIDC set:
| Claim | Description |
|---|---|
keyauth | <challenge-token>.<acme-account-key-thumbprint> |
acme_aud | URL of the OIDC challenge endpoint on the ACME server |
name | User’s display name |
preferred_username | Wire handle (e.g. [email protected]) |
keyauth and acme_aud, and ensures name and preferred_username match the values submitted in the order.
X.509 certificate structure
The certificate issued by step-ca encodes both identifiers as Subject Alternative Name (SAN) URIs:Credential types
CoreCrypto supports two MLS credential types:| Type | Description | E2EI state |
|---|---|---|
CredentialType::Basic | A self-asserted keypair with no external attestation. Used before E2EI enrollment or when a backend does not enforce E2EI. | NotEnabled |
CredentialType::X509 | An X.509 certificate bound to a Wire client ID and user handle, issued by the organization’s step-ca instance. | Verified (when valid) |
E2eiConversationState reflects the aggregate state.
E2eiConversationState
TheE2eiConversationState enum describes the E2EI posture of an MLS conversation at a point in time. It is returned by get_credential_in_use and e2ei_verify_group_state.
E2eiConversationState reflects the committed state of the group. Pending commits and pending proposals are not considered. A commit adding an unverified member will change the state only once the commit is applied.| State | Meaning |
|---|---|
Verified | Every leaf node in the ratchet tree carries a valid X.509 credential. Safe to display a full verification badge. |
NotVerified | The group is mixed: some members have X.509 credentials (possibly expired), others have Basic credentials. Display a partial/warning badge. |
NotEnabled | No member has an X.509 credential. E2EI has not been deployed for this conversation. |
WireIdentity and X509Identity
When you inspect the identity of a group member, CoreCrypto returns aWireIdentity struct:
x509_identity is populated with:
Checking conversation E2EI state
To check the E2EI state of a conversation you already belong to, useget_credential_in_use on the transaction context:
GroupInfo message), use e2ei_verify_group_state. This method applies hardened ratchet tree verification as a sender, so it detects tampered group state:
Certificate rotation
When an existing client renews its X.509 certificate (e.g. before expiry or after a handle change), it usessave_x509_credential followed by credential rotation in each conversation.
After calling save_x509_credential, the returned CredentialRef must be applied to every conversation via ConversationGuard::set_credential_by_ref, new key packages must be generated, and old credentials and key packages must be removed from the backend: