This is the second in a series of weekly updates detailing progress on bringing MLS protocol DMs and group messaging to Nostr.
Previous Updates
- June 28th 2024
Progress this week
This week was mostly spent on the topic of how to properly publish prekey bundles and what would be needed in the bundle itself to make it workable. I’ve included an early version of the spec below for prekeys, and would love thoughts on it. Treat this as an alpha version, very subject to change.
The other thing I spent time on was making changes to the OpenMLS library to add support for our custom ciphersuite. One issue that I’ve run into is that the IETF standard for HPKE doesn’t include the secp256k1 curve. Because of this, the library being used in the OpenMLS library doesn’t implement the necessary methods using our curve. Thankfully, there is another library with an open PR (shout out to bitgould (npub1yev…qrkl) for that!) that would fix this. Additionally, there’s an expired proposal to add secp256k1 to the HPKE spec itself. I’ve bumped both of these and will continue to follow up. Even without the formal addition to the spec, if we have a working library, I can add that to the OpenMLS library.
Spec Draft for Prekeys
Initial keying material (Prekey Event)
Each user that wishes to be reachable via MLS-based messaging MUST first publish a prekey event. Prekeys are used to authenticate and add members to groups (one-to-one DMs or groups with more than two participants) in an asynchronous way. The prekey event is a simple replaceable event and contains all the information needed to add a user to a group.
Prekeys SHOULD be used only once. Resuse of prekeys can lead to replay attacks.
In most cases, clients that implement this NIP will manage the creation and rotation of the prekey event. It’s recommended that clients do so interactively with user consent in order to avoid overwriting prekeys created by other clients.
Derived vs Ephemeral Prekeys
Since prekeys are generated on a single device/client pair, the private key of the prekey must be either stored or generated in a way that can be deterministically recovered.
The recommended approach is to use derived keys, generated in the manner described in NIP-06. In this way, the user can respond to a new group request from any device/client pair, not just from the same device/client pair that created the initial prekey event. If using derived keys;
- Clients MUST use
104
as theaccount
level value and0
at thechange
level value (e.g.m/44'/1237'/104'/0/0
). - Keys are then generated using public derivation by incrementing the
address_index
level value. - Clients MUST include the full derivation path corresponding to the key used in the
content
field on the prekey event. - The
content
field MUST be encrypted using standard NIP-44 encryption (encrypted to themselves).
However, for added security (and consequently a more restrictive user experience), clients can chose to generate an ephemeral key and store the private key locally. This means that users will only be able to respond to new group requests from the same device/client pair and won’t be able to respond at all if the prekey’s private key is lost. Ephemeral keys can also be used with minimal degredation of UX if you’re using a remote signer that can manage these keys.
If using an ephemeral key;
- The
content
field on the prekey event MUST be filled in withEPHEMERAL
and then encrypted using standard NIP-44 encryption (encrypted to themselves). This ensures that anyone looking at prekey events cannot tell whether it’s a derived or an ephemeral prekey.
Example Prekey Event
{
"id": <id>,
"kind": 10443,
"created_at": <unix timestamp in seconds>,
"pubkey": <main identity pubkey>,
"content": <encrypted derivation path | EPHEMERAL>,
"tags": [
["mls_protocol_version", "1.0"],
["ciphersuite", "MLS_256_DHKEMK256_CHACHA20POLY1305_SHA256_K256"],
["pubkey", <prekey pubkey>],
["prekey_sig", <signature generated from hex encoded pubkey of the prekey>],
["r", "wss://nos.lol"],
["r", "wss://relay.primal.net"]
],
"sig": <signed with main identity key>
}
Tags
- The
mls_protocol_version
tag identifies the MLS protocol version being used. For now, this MUST be1.0
- The
ciphersuite
tag identifies the ciphersuite supported. For now on Nostr, we’re using a custom ciphersuite,MLS_256_DHKEMK256_CHACHA20POLY1305_SHA256_K256
. Read more about ciphersuites in MLS. pubkey
is the derived or ephemeral prekey pubkey.- The
prekey_sig
tag value is a Schnorr signature (over the secp256k1 curve) of the SHA-256 hashed value of the prekey’s pubkey, signed with the prekey’s private key.
const privKey = schnorr.utils.randomPrivateKey();
const pubKey = schnorr.getPublicKey(privKey);
const prekeySig = bytesToHex(
schnorr.sign(bytesToHex(sha256(pubKey)), privKey)
);
const prekeyTag = ["prekey_sig", prekeySig];
Finally, clients SHOULD include r
tags to identify each of the relays that they will attempt to publish this prekey event to. This allows for more complete replacement of prekey events at a later date.
Replacing Prekey Events
Clients MUST replace the prekey event on all the listed relays any time they successfully process a group welcome event. If the prekey was a derived prekey, clients SHOULD increment the derivation path by 1 for the next key.
Onward and Upward
This next week I’ll continue to work on getting the right curves and code added to the OpenMLS library and start work on a simple demo app. The focus is on better understanding what we need from the control and message events and how we can make those as simple as possible for Nostr clients and relays while also preserving as much privacy as possible.