Integrating Germ with AT Protocol

A pink and green graphic reads "Integrating Germ with AT Protocol." There is the MLS logo, the Germ globe logo, and a small diagram showing cartoon germs with keys and arrows between them

End-to-end encrypted Germ DM is now a Bluesky messenger. Germ's integration with AT Protocol, the open social protocol powering Bluesky and other apps across the "ATmosphere," opens in beta today. Join the waitlist here and be first to try it. For a wider sense of the integration’s user experience, check out our companion announcement post.

In this technical post, we introduce the architecture of Germ’s integration with AT Protocol. From the beginning, Germ has been building for an interoperable E2EE ecosystem; this extends to our integration with AT Protocol. We’ll define the goals that we set for this integration, detail our first steps toward those goals, and outline our path forward.

Have more questions for us? Join Germ’s co-founders for a Town Hall Tuesday, August 5 at 11am PST—register here!


Germ now enables users Alice and Bob who have ATProtocol accounts to exchange E2EE messages. To do this:

  1. Alice and Bob have to agree on messaging cryptographic identities for each other, which they can be assured are more tightly controlled by the user than the AT Protocol handle.

    • Commonly, we refer to these identities as device-bound to indicate that only the computing devices for a particular user can use the identity, as opposed to keys that may be delegated to a server, as AT Protocol signing keys commonly are.

  2. Alice and Bob have to be able to set up encryption sessions that terminate in each other’s messaging cryptographic identities.

  3. Alice and Bob have to be able to transport these encrypted messages to each other.

For each step, we’ll identify the functional goals, describe how it is implemented in our app today, and outline how we think an interoperable protocol can be built to meet those functional goals.

AT Protocol Identity

First, a brief intro to AT Protocol identities. People interact with AT Protocol handles such as @germnetwork.com. Users can change their own handle. These handles resolve via DNS to a stable underlying Distributed Identifier (DID) like did:plc:4yvwfwxfz5sney4twepuzdu7. This DID is the stable identifier for an ATProto account.

The goal of Germ’s integration is to provide end-to-end encrypted conversations between AT Protocol DIDs. Conversations won’t be interrupted by users changing their AT Protocol handles. Users may (and should) regularly update the keys they use for E2EE identity and encryption, and notices may be required if those key changes introduce a security boundary that users should be aware of and can take some action to.

1. Delegation by the DID to an E2EE Identity (Key)

AT Protocol originates as a protocol for publishing microblogs. Its developers have mentioned their architectural insight in delegating the keys used for peer-to-peer operations to a Personal Data Server (PDS). This is a great fit for publishing. If a malicious or compromised PDS uses those keys in an unexpected way (for example, by publishing a post the user didn’t author), the user is expected to quickly detect and rectify this misuse. However, these publishing keys are not suitable for use for private communication, where users cannot detect misuse of delegated keys to read or author private messages.

How can we delegate from these publishing keys to an identity key suitable for E2EE messaging? The basic principle underpinning phone-number-indexed E2EE messaging deployed for billions of users is:

Alice trusts the key she obtains for Bob from a weakly trusted intermediary Dawn, if Alice can expect that Bob will notice (quickly) if Dawn is lying.

  • The basic iteration of this is that users (E2EE messaging apps acting on the user’s behalf) monitor the key directory for their own keys and are alerted if they detect a mismatch.

  • The robust improvement on this in recent years is a key transparency (KT) log, which provides stronger assurances that a key directory has been honestly vending keys consistently to all requesters.

Functional Goals

Our implementation has the following goals:

  • Each DID binds to a public signing key (we call this an Anchor key; in our app UI we refer to this as a pairing key) for E2EE messaging (or nil to represent they are not capable of receiving E2EE messages).

    • The value of this binding may change over time

    • This key must in turn bind to the ATProto identity, completing the bidirectional binding of a DID to a user-controlled identity key.

  • Users not using Germ would notice if their DID binds to a Germ Anchor Key.

    • E.g. If someone with temporary access to a user’s ATProto account tried to masquerade as the user in E2EE conversations on Germ.

  • Users can monitor this binding to detect and react to changes to the bound Anchor Key.

Germ’s Implementation Today

Germ binds a DID to an Anchor Key by publishing the user’s current Anchor Key in the AT Protocol profile text. The Germ app monitors the user's own bio and others’ for changes to their Anchor Keys.

By putting this binding in the user-visible bio, we gain the assurance that users not on Germ will notice if a malicious app or user poses as them in private conversations. We considered and rejected an alternative where we placed the binding in a custom PDS record, which users would not see on other AppViews.

In this way, we’re bootstrapping a key directory off the user-facing AT Protocol profile.

Verifying the delegation

When Bob observes an anchor key in Alice’s AT Protocol bio, he understands that Alice’s DID intends for this anchor key to represent Alice in E2EE conversations. Bob can finish this verification by fetching the com.germnetwork.keypackage record from Alice’s PDS; this record contains Alice’s AnchorHello package. This AnchorHello contains data that allows Bob to set up an encryption session with Alice. It also includes a signature by Alice’s Anchor Key over Alice’s DID, completing this bidirectional binding.

Discussion and Future Directions

The important conceptual properties of our desired key directory are:

  • At any point in time, a DID maps to an Anchor Key (or none)

  • Everyone has a consistent view of this directory

  • The value of the bound Anchor Key will change over time

  • The sequential changes to these values are recorded in an immutable log

We can meet most of these goals by leveraging the role of PDS as a publisher. By publishing changes to the bound Anchor Key (including to and from nil - i.e., setting up a new key or ceasing to use E2EE messaging for this DID), the PDS can form a time-series of a user’s anchor keys as they change over time.

We could move the Anchor Key out of the ATProto bio, if we had a standardized way for PDS’s to publish an E2EE key, and a corresponding assurance that most AppViews can give users transparency for their current anchor key and allow the user to remove unexpected values,

A key concept for the remainder of our dive down the layers is that:

At the application layer, a valid message from Alice to Bob is sent from (a delegate of) Alice’s current Anchor Key to (a delegate of) Bob’s current Anchor key.

In this sense, Germ’s anchor key is an application layer identity, representing the user-facing entity that is (currently) conversing as this ATProto Identity.

2. Setting Up Encryption Session(s)

Now that Bob has verified Alice’s current Anchor Key, and has published his own Anchor key, Bob can set up an encryption session with Alice. We don’t directly use the Anchor Key to do so, but instead delegate down to a task-designated signing key we call an Agent.

The Anchor Key is analogous to an Identity Key in the Autonomous Communicator Protocol (ACP), whose role is to unify multiple agents behind a single user-facing representation. (Read more about the mechanics of this session in our blog post on the AC protocol.)

These agents serve the functional role of an endpoint for encryption sessions and transport paths, allowing us to decouple the application layer, user-facing identity, from session encryption and transport paths.

For example, the handshake to set up an ATProtocol conversation goes through the following steps:

  • In the AnchorHello, Alice's Anchor delegates a Hello agent to receive handshakes for new encryption sessions.

  • When Bob uses Alice’s AnchorHello to start a new conversation, Bob’s Anchor delegates to a new messaging Agent for a new Bob-Alice encryption session.

    • The first round of messages from Bob to Alice are encrypted to Alice’s Hello Agent (precisely, to the HPKE init key in an MLS keyPackage for Alice’s Hello Agent). These messages include a Reply object that includes Bob’s newly delegated agent, and signatures proving the delegation from DID, to Anchor, to Agent.

    • On receipt, Alice can check these signatures and compare Bob’s Anchor agent to his AT Protocol bio.

  • Since Alice’s Hello Agent may receive many incoming new connections, Alice will hand off each new session to a newly delegated agent.

Agents map 1:1 to MLS Clients, and we perform this handoff with MLS update operations. We ensure this bidirectional binding of Agents to MLS clients by:

  • using the Agent’s public key as a basic credential for the MLS Client, so that the MLS Client’s signing key signs over the basic credential, e.g. in a MLS keyPackage message or MLS proposals.

  • Ensuring we always sign over MLS keyPackage or Proposal messages that introduce new MLS leafNodes, with the corresponding Agent key.

Distributed MLS

We use Messaging Layer Security (MLS) as the encryption primitive for our encryption session, though the session is not simply one MLS group. MLS requires that group state advance linearly through epochs, distilling the problem of group consensus down to resolving which proposed change (MLS Commit) advances the group into the next epoch. The Delivery Service in MLS Architecture fills the role of adjudicating competing commits to end the same epoch, and is commonly implemented with centralized state for each MLS group.

Our goal in designing the encryption session was to allow participants to quickly introduce new randomness into the group state, without introducing centralized state which would pose an obstacle to interoperability.

While our initial deployment of MLS used a scheme where Alice and Bob take turns committing to a single MLS group, that approach isn’t extensible to group conversations. We also encountered other challenges with sharing group state in this simplest distributed system (two instances of an app communicating over an unreliable, stateless transport medium)

In collaboration with other MLS implementers with distributed applications, we came up with a meta-construction atop MLS for sending group messages in distributed systems: https://datatracker.ietf.org/doc/draft-xue-distributed-mls/

In this construction, each participant uses an MLS group as their Send Group to encrypt messages to other members. Users can introduce fresh key material for Post Compromise Security (PCS) across groups using the built-in key import and export mechanisms of MLS.

We shipped a 1:1 implementation of Distributed MLS earlier this year 2.2.0, and have already seen significant reliability improvements over our initial deployment. We will shortly publish a detailed writeup of this implementation, which we call TwoMLS.

Setting up a TwoMLS group

So in the Hello/Reply handshake, at the encryption session layer:

  • Bob uses Alice’s AnchorHello keypackage to add her Hello Agent to a new MLS group with himself as the only other member. This is Bob’s send group to Alice.

  • Bob can encrypt Reply messages to Alice using his send group. In this first round, Bob staples to each message the MLS welcome that allows Alice to join Bob’s send group and decrypt the first round of messages.

  • On receipt of a message in this first round, Alice’s Hello Agent can join Bob’s send group. Alice can export a key from Bob’s send group and incorporate it when forming her send group. In this way, only a member of Bob’s send group can join Alice’s send group.

  • In her first round of messages, Alice will send a welcome to her new send group, and propose an MLS update of her client in Bob’s send group, to a newly generated agent for the Alice-Bob session.

These two MLS send groups comprise a TwoMLS session. In TwoMLS, parties propose updates to the opposite send group, and commit to both send groups on each round-trip. In this way they introduce and incorporate fresh key material for PCS on each round trip.

Session Management

We can’t assume that this new session will be the only session that exists between Alice and Bob. They could, for example, simultaneously form Replies to each other’s AnchorHello, each independently setting up a new TwoMLS encryption session. Users could also recover from session data corruption by setting up a new session with the other party’s AnchorHello.

The application layer must allow for multiple valid sessions in concurrent use between a given pair of DID’s.

For efficient introduction of new entropy, users should converge on a common session that they use, which we do with a most recently used algorithm. Clients can purge sessions that have not been recently used, which they can safely do if they can reconnect by establishing a new session with the other party’s AnchorHello.

Users can also expire sessions by rotating their anchor key, invalidating sessions created with delegates of the previous anchor. To reduce spurious user warnings, we can allow the current and replacement key to sign over a replacement operation, and use MLS update operations to update existing sessions to terminate in agents of the new anchor key.

Key Rotation

MLS proposal and commit (as well as the pre-shared key (PSK) import and export we use in Distributed MLS) operations introduce new encryption material into these sessions. Users should also rotate the identity keys they use. MLS operations to propose and commit changes to member identities, allows us to update the identities that our encryption sessions terminate in - whether for functional reasons (handing off from an invitation agent to a dedicated agent per session), or to rotate the anchor key.

This allows us to rotate the published Anchor key without spurious security warnings

  • If Alice publishes a new anchor key, while in possession of the predecessor, she can choose to also publish a proof of succession between the predecessor and successor.

  • Alice can then delegate new agents from the new anchor, and use MLS operations to propose updates to new agents, bringing existing sessions in line with the currently published anchor key.

Otherwise, if Bob(’s app) observes changes to Alice’s key but is unable to obtain proof that the predecessor and successor anchor keys both endorse this succession, Bob’s app should warn Bob. For example, Alice may have lost her old device and has to publish a new anchor key without access to the previous key. Alice may also be deliberately signaling a recovery from local state compromise by withholding the new key’s endorsement of previous state. Either way, Bob should understand that he has crossed a security boundary, and may need to adjust his assessment of who controls Alice’s current anchor key.

Encryption Overview

From the top down:

  • The DID binds to a user’s Anchor Key by publishing the user’s current Anchor key.

    • PDS signing ensures this is a cryptographic binding by the DID to the Anchor Key.

    • Users are expected to periodically rotate their Anchor Key.

  • The Anchor key:

    • Proves its binding to the DID by signing over it, for example in an AnchorHello object

    • Delegates Agents to function as MLS clients

      • Delegates one designated agent to receive new connection requests as a Hello agent

        • Advertises an MLS keyPackage in an AnchorHello package posted as the com.germnetwork.keypackage record

        • This keyPackage and agent can be rotated periodically

    • Agents form Distributed MLS sessions between each other, using an MLS group to encrypt messages to recipients, and exporting entropy across MLS send groups.

  • Two users may have multiple Distributed MLS sessions between their Anchor keys

    • Users should converge on a common one, expire old sessions (so they can drop stale private keys), and reconnect as necessary through other parties’ current AnchorHello.

Message Transport

Reduced Metadata Transport

TwoMLS protects MLS metadata, such as the MLS group identifier and epoch, by encrypting MLS private messages with a symmetric header key derived from each MLS epoch. The only functional requirement on our transport layer is that recipients should know which session incoming cipher texts are intended for.

The ACP transport protocol meets this requirement without exposing session metadata, using destination addresses whose mappings to session are only known by the agents at each end of the session. ACP uses both ephemeral mailboxes, and rendezvous mailboxes at addresses that are derived from the epoch and change on every message exchange. Germ implements these transport paths over HTTPS, but the concept is flexible enough to support multiple concurrent transport paths and technologies. We’re especially interested in exploring local routing as a complement to the Internet.

All messages after the initial round are transported via the ACP transport protocol.

Initial Authentication

For the first round of messages, we choose to authenticate the first round of Reply messages to the sender’s Anchor key, and address them in the plaintext envelope to the recipient’s Anchor key. Germ users register with Germ to receive messages for their Anchor key, but do not tell Germ what DID this anchor key represents.

We chose to do this as it allows us to better protect a user’s inbox for new connections against malicious mailbox stuffing. We believe this also shadows a potential role for PDS to vend single-use keyPackages to authorized requesters.

Many users only want to accept new messages from a subset of AT Protocol users, e.g. accounts they follow. For a consistent user experience, this policy is public, so that a remote party will know if their message could be received. For example, AppViews won’t display a DM button on a profile if the recipient’s new connection policy excludes the viewer. We include this policy in the AnchorHello, so the remote party’s Germ App can correctly set expectations.

In our initial implementation, the user’s policy on allowable new connections is enforced on the recipient’s device, and the advertised policy deters remote parties from sending a connection request that will be dropped. Germ’s servers don’t know our users’ DID’s or their social graph, and can’t apply that policy.

The PDS, which already knows my AT Protocol social graph, is in a good position to bounce connection requests that I won’t accept, and could even vend single use keyPackages to authenticated requesters. This presumes an authenticated xRPC interface which other apps have proposed, e.g. https://gist.github.com/ngerakines/f50c17a2c2d50d67c0276aa54147ab22

Metadata

When you pair your Germ App with AT Protocol, Germ’s servers learn only your anchor key. Germ servers never learn your ATProto DID. Germ mediates the authenticated delivery of initial reply messages from an anchor key to an anchor key, but afterwards all messages are exchanged with ephemeral or rendezvous addresses.

Users will be able to bypass the initial authenticated Reply by directly exchanging their AT Protocol cards via a QR code or link, in which case the Hello and Reply are exchanged as described in the Autonomous Communicator Protocol blog post.

Looking Forward

We believe this is a robust foundation for E2EE with ATProto Identities. The delegation of the ATProto DID to Anchor Keys over time is a key primitive that allows for a myriad of implementations. An Anchor Key, for example, can bootstrap a device set to allow users to use multiple of their own devices to participate in E2EE messaging. We look forward to the continued exchange of ideas with other developers building on ATProto to find a common path forward.

Previous
Previous

Germ DM x ATProto is Now in Beta

Next
Next

Join the beta waitlist: Germ DM is integrating with your Bluesky handle!