mirror of
https://github.com/vacp2p/rfc-index.git
synced 2026-01-10 08:08:17 -05:00
Compare commits
3 Commits
nom-signin
...
mls-rfc-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dd0a14aff | ||
|
|
29bdce4d57 | ||
|
|
1891b712dd |
@@ -1,889 +0,0 @@
|
||||
---
|
||||
title: VAC-DECENTRALIZED-MESSAGING-ETHEREUM
|
||||
name: Decentralized Key and Session Setup for Secure Messaging over Ethereum
|
||||
status: raw
|
||||
category: informational
|
||||
editor: Ramses Fernandez-Valencia <ramses@status.im>
|
||||
contributors:
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document introduces a decentralized group messaging protocol
|
||||
using Ethereum adresses as identifiers.
|
||||
It is based in the proposal
|
||||
[DCGKA](https://eprint.iacr.org/2020/1281) by Weidner et al.
|
||||
It includes also approximations to overcome limitations related to using PKI and
|
||||
the multi-device setting.
|
||||
|
||||
## Motivation
|
||||
|
||||
The need for secure communications has become paramount.
|
||||
Traditional centralized messaging protocols are susceptible to various security
|
||||
threats, including unauthorized access, data breaches, and single points of
|
||||
failure.
|
||||
Therefore a decentralized approach to secure communication becomes increasingly
|
||||
relevant, offering a robust solution to address these challenges.
|
||||
|
||||
Secure messaging protocols used should have the following key features:
|
||||
|
||||
1. **Asynchronous Messaging:** Users can send messages even if the recipients
|
||||
are not online at the moment.
|
||||
|
||||
2. **Resilience to Compromise:** If a user's security is compromised,
|
||||
the protocol ensures that previous messages remain secure through forward
|
||||
secrecy (FS). This means that messages sent before the compromise cannot be
|
||||
decrypted by adversaries. Additionally, the protocol maintains post-compromise
|
||||
security (PCS) by regularly updating keys, making it difficult for adversaries
|
||||
to decrypt future communication.
|
||||
|
||||
3. **Dynamic Group Management:** Users can easily add or remove group members
|
||||
at any time, reflecting the flexible nature of communication within the app.
|
||||
|
||||
In this field, there exists a *trilemma*, similar to what one observes in
|
||||
blockchain, involving three key aspects:
|
||||
|
||||
1. security,
|
||||
2. scalability, and
|
||||
3. decentralization.
|
||||
|
||||
For instance, protocols like the [MLS](https://messaginglayersecurity.rocks)
|
||||
perform well in terms of scalability and security.
|
||||
However, they falls short in decentralization.
|
||||
|
||||
Newer studies such as [CoCoa](https://eprint.iacr.org/2022/251)
|
||||
improve features related to security and scalability,
|
||||
but they still rely on servers, which may not be fully trusted though they are necessary.
|
||||
|
||||
On the other hand,
|
||||
older studies like [Causal TreeKEM](https://mattweidner.com/assets/pdf/acs-dissertation.pdf)
|
||||
exhibit decent scalability (logarithmic)
|
||||
but lack forward secrecy and have weak post-compromise security (PCS).
|
||||
|
||||
The creators of [DCGKA](https://eprint.iacr.org/2020/1281) introduce a decentralized,
|
||||
asynchronous secure group messaging protocol that supports dynamic groups.
|
||||
This protocol operates effectively on various underlying networks
|
||||
without strict requirements on message ordering or latency.
|
||||
It can be implemented in peer-to-peer or anonymity networks,
|
||||
accommodating network partitions, high latency links, and
|
||||
disconnected operation seamlessly.
|
||||
Notably, the protocol doesn't rely on servers or
|
||||
a consensus protocol for its functionality.
|
||||
|
||||
This proposal provides end-to-end encryption with forward secrecy and
|
||||
post-compromise security,
|
||||
even when multiple users concurrently modify the group state.
|
||||
|
||||
## Theory
|
||||
|
||||
### Protocol overview
|
||||
|
||||
This protocol makes use of ratchets to provide FS
|
||||
by encrypting each message with a different key.
|
||||
|
||||
In the figure one can see the ratchet for encrypting a sequence of messages.
|
||||
The sender requires an initial update secret `I_1`, which is introduced in a PRG.
|
||||
The PRG will produce two outputs, namely a symmetric key for AEAD encryption, and
|
||||
a seed for the next ratchet state.
|
||||
The associated data needed in the AEAD encryption includes the message index `i`.
|
||||
The ciphertext `c_i` associated to message `m_i`
|
||||
is then broadcasted to all group members.
|
||||
The next step requires deleting `I_1`, `k_i` and any old ratchet state.
|
||||
|
||||
After a period of time the sender may replace the ratchet state with new update secrets
|
||||
`I_2`, `I_3`, and so on.
|
||||
|
||||
To start a post-compromise security update,
|
||||
a user creates a new random value known as a seed secret and
|
||||
shares it with every other group member through a secure two-party channel.
|
||||
Upon receiving the seed secret,
|
||||
each group member uses it to calculate an update secret for both the sender's ratchet
|
||||
and their own.
|
||||
Additionally, the recipient sends an unencrypted acknowledgment to the group
|
||||
confirming the update.
|
||||
Every member who receives the acknowledgment updates
|
||||
not only the ratchet for the original sender but
|
||||
also the ratchet for the sender of the acknowledgment.
|
||||
Consequently, after sharing the seed secret through `n - 1` two-party messages and
|
||||
confirming it with `n - 1` broadcast acknowledgments,
|
||||
every group member has derived an update secret and updated their ratchet accordingly.
|
||||
|
||||
When removing a group member,
|
||||
the user who initiates the removal conducts a post-compromise security update
|
||||
by sending the update secret to all group members except the one being removed.
|
||||
To add a new group member,
|
||||
each existing group member shares the necessary state with the new user,
|
||||
enabling them to derive their future update secrets.
|
||||
|
||||
Since group members may receive messages in various orders,
|
||||
it's important to ensure that each sender's ratchet is updated consistently
|
||||
with the same sequence of update secrets at each group member.
|
||||
|
||||
The network protocol used in this scheme ensures that messages from the same sender
|
||||
are processed in the order they were sent.
|
||||
|
||||
### Components of the protocol
|
||||
|
||||
This protocol relies in 3 components:
|
||||
authenticated causal broadcast (ACB),
|
||||
decentralized group membership (DGM) and
|
||||
2-party secure messaging (2SM).
|
||||
|
||||
#### Authenticated causal broadcast
|
||||
|
||||
A causal order is a partial order relation `<` on messages.
|
||||
Two messages `m_1` and `m_2` are causally ordered, or
|
||||
`m_1` causally precedes `m_2`
|
||||
(denoted by `m_1 < m_2`), if one of the following contiditions hold:
|
||||
|
||||
1. `m_1` and `m_2` were sent by the same group member, and
|
||||
`m_1` was sent before `m_2`.
|
||||
2. `m_2` was sent by a group member U, and `m_1` was received and
|
||||
processed by `U` before sending `m_2`.
|
||||
3. There exists `m_3` such that `m_1 < m_3` and `m_3 < m_2`.
|
||||
|
||||
Causal broadcast requires that before processing `m`, a group member must
|
||||
process all preceding messages `{m' | m' < m}`.
|
||||
|
||||
The causal broadcast module used in this protocol authenticates the sender of
|
||||
each message, as well as its causal ordering metadata, using a digital
|
||||
signature under the sender’s identity key.
|
||||
This prevents a passive adversary from impersonating users or affecting
|
||||
causally ordered delivery.
|
||||
|
||||
#### Decentralized group membership
|
||||
|
||||
This protocol assumes the existence of a decentralized group membership
|
||||
function (denoted as DGM) that takes a set of membership change messages and
|
||||
their causal order relantionships, and returns the current set of group
|
||||
members’ IDs. It needs to be deterministic and depend only on causal order, and
|
||||
not exact order.
|
||||
|
||||
#### 2-party secure messaging (2SM)
|
||||
|
||||
This protocol makes use of bidirectional 2-party secure messaging schemes,
|
||||
which consist of 3 algorithms: `2SM-Init`, `2SM-Send` and `2SM-Receive`.
|
||||
|
||||
##### Function 2SM-Init
|
||||
|
||||
This function takes two IDs as inputs:
|
||||
`ID1` representing the local user and `ID2` representing the other party.
|
||||
It returns an initial protocol state `sigma`.
|
||||
The 2SM protocol relies on a Public Key Infrastructure (PKI) or
|
||||
a key server to map these IDs to their corresponding public keys.
|
||||
In practice, the PKI should incorporate ephemeral prekeys.
|
||||
This allows users to send messages to a new group member,
|
||||
even if that member is currently offline.
|
||||
|
||||
##### Function 2SM-Send
|
||||
|
||||
This function takes a state `sigma` and a plaintext `m` as inputs, and returns
|
||||
a new state `sigma’` and a ciphertext `c`.
|
||||
|
||||
##### Function 2SM-Receive
|
||||
|
||||
This function takes a state `sigma` and a ciphertext `c`, and
|
||||
returns a new state `sigma’` and a plaintext `m`.
|
||||
|
||||
This function takes a state `sigma` and a ciphertext `c`, and returns a new
|
||||
state `sigma’` and a plaintext `m`.
|
||||
|
||||
#### Function 2SM Syntax
|
||||
|
||||
The variable `sigma` denotes the state consisting in the variables below:
|
||||
|
||||
```text
|
||||
sigma.mySks[0] = sk
|
||||
sigma.nextIndex = 1
|
||||
sigma.receivedSk = empty_string
|
||||
sigma.otherPk = pk`<br>
|
||||
sigma.otherPksender = “other”
|
||||
sigma.otherPkIndex = 0
|
||||
|
||||
```
|
||||
|
||||
#### 2SM-Init
|
||||
|
||||
On input a key pair `(sk, pk)`, this functions otuputs a state `sigma`.
|
||||
|
||||
#### 2SM-Send
|
||||
|
||||
This function encrypts the message `m` using `sigma.otherPk`, which represents
|
||||
the other party’s current public key.
|
||||
This key is determined based on the last public key generated for the other
|
||||
party or the last public key received from the other party,
|
||||
whichever is more recent. `sigma.otherPkSender` is set to `me` in the former
|
||||
case and `other` in the latter case.
|
||||
|
||||
Metadata including `otherPkSender` and `otherPkIndex` are included in the
|
||||
message to indicate which of the recipient’s public keys is being utilized.
|
||||
|
||||
Additionally, this function generates a new key pair for the local user,
|
||||
storing the secret key in `sigma.mySks` and sending the public key.
|
||||
Similarly, it generates a new key pair for the other party,
|
||||
sending the secret key (encrypted) and storing the public key in
|
||||
`sigma.otherPk`.
|
||||
|
||||
```text
|
||||
sigma.mySks[sigma.nextIndex], myNewPk) = PKE-Gen()
|
||||
(otherNewSk, otherNewPk) = PKE-Gen()
|
||||
plaintext = (m, otherNewSk, sigma`.nextIndex, myNewPk)
|
||||
msg = (PKE-Enc(sigma.otherPk, plaintext), sigma.otherPkSender, sigma.otherPkIndex)
|
||||
sigma.nextIndex++
|
||||
(sigma.otherPk, sigma.otherPkSender, sigma.otherPkIndex) = (otherNewPk, "me", empty_string)
|
||||
return (sigma`, msg)
|
||||
|
||||
```
|
||||
|
||||
#### 2SM-Receive
|
||||
|
||||
This function utilizes the metadata of the message `c` to determine which
|
||||
secret key to utilize for decryption, assigning it to `sk`.
|
||||
If the secret key corresponds to one generated by ourselves,
|
||||
that secret key along with all keys with lower index are deleted.
|
||||
This deletion is indicated by `sigma.mySks[≤ keyIndex] = empty_string`.
|
||||
Subsequently, the new public and secret keys contained in the message are
|
||||
stored.
|
||||
|
||||
```text
|
||||
(ciphertext, keySender, keyIndex) = c
|
||||
if keySender = "other" then
|
||||
sk = sigma.mySks[keyIndex]
|
||||
sigma.mySks[≤ keyIndex] = empty_string
|
||||
else sk = sigma.receivedSk
|
||||
(m, sigma.receivedSk, sigma.otherPkIndex, sigma.otherPk) = PKE-Dec(sk, ciphertext)
|
||||
sigma.otherPkSender = "other"
|
||||
return (sigma, m)
|
||||
|
||||
```
|
||||
|
||||
### PKE Syntax
|
||||
|
||||
The required PKE that MUST be used is ElGamal with a 2048-bit modulus `p`.
|
||||
|
||||
#### Parameters
|
||||
|
||||
The following parameters must be used:
|
||||
|
||||
```text
|
||||
p = 308920927247127345254346920820166145569
|
||||
g = 2
|
||||
|
||||
```
|
||||
|
||||
#### PKE-KGen
|
||||
|
||||
Each user `u` MUST do the following:
|
||||
|
||||
```text
|
||||
PKE-KGen():
|
||||
a = randint(2, p-2)
|
||||
pk = (p, g, g^a)
|
||||
sk = a
|
||||
return (pk, sk)
|
||||
|
||||
```
|
||||
|
||||
#### PKE-Enc
|
||||
|
||||
A user `v` encrypting a message `m` for `u` MUST follow these steps:
|
||||
|
||||
```text
|
||||
PKE-Enc(pk):
|
||||
k = randint(2, p-2)
|
||||
eta = g^k % p
|
||||
delta = m * (g^a)^k % p
|
||||
return ((eta, delta))
|
||||
|
||||
```
|
||||
|
||||
#### PKE-Dec
|
||||
|
||||
The user `u` recovers a message `m` from a ciphertext `c`
|
||||
by performing the following operations:
|
||||
|
||||
```text
|
||||
PKE-Dec(sk):
|
||||
mu = eta^(p-1-sk) % p
|
||||
return ((mu * delta) % p)
|
||||
|
||||
```
|
||||
|
||||
### DCGKA Syntax
|
||||
|
||||
#### Auxiliary functions
|
||||
|
||||
There exist 6 functions that are auxiliary for the rest of components of the
|
||||
protocol, namely:
|
||||
|
||||
#### init
|
||||
|
||||
This function takes an `ID` as input and returns its associated initial state,
|
||||
denoted by `gamma`:
|
||||
|
||||
```text
|
||||
gamma.myId = ID
|
||||
gamma.mySeq = 0
|
||||
gamma.history = empty
|
||||
gamma.nextSeed = empty_string
|
||||
gamma.2sm[·] = empty_string
|
||||
gamma.memberSecret[·, ·, ·] = empty_string
|
||||
gamma.ratchet[·] = empty_string
|
||||
return (gamma)
|
||||
|
||||
```
|
||||
|
||||
#### encrypt-to
|
||||
|
||||
Upon reception of the recipient’s `ID` and a plaintext, it encrypts a direct
|
||||
message for another group member.
|
||||
Should it be the first message for a particular `ID`,
|
||||
then the `2SM` protocol state is initialized and stored in
|
||||
`gamma.2sm[recipient.ID]`.
|
||||
One then uses `2SM_Send` to encrypt the message and store the updated protocol
|
||||
in `gamma`.
|
||||
|
||||
```text
|
||||
if gamma.2sm[recipient_ID] = empty_string then
|
||||
gamma.2sm[recipient_ID] = 2SM_Init(gamma.myID, recipient_ID)
|
||||
(gamma.2sm[recipient_ID], ciphertext) = 2SM_Send(gamma.2sm[recipient_ID], plaintext)
|
||||
return (gamma, ciphertext)
|
||||
|
||||
```
|
||||
|
||||
#### decrypt-from
|
||||
|
||||
After receiving the sender’s `ID` and a ciphertext, it behaves as the reverse
|
||||
function of `encrypt-to` and has a similar initialization:
|
||||
|
||||
```text
|
||||
if gamma.2sm[sender_ID] = empty_string then
|
||||
gamma.2sm[sender_ID] = 2SM_Init(gamma.myID, sender_ID)
|
||||
(gamma.2sm[sender_ID], plaintext) = 2SM_Receive(gamma.2sm[sender_ID], ciphertext)
|
||||
return (gamma, plaintext)
|
||||
|
||||
```
|
||||
|
||||
#### update-ratchet
|
||||
|
||||
This function generates the next update secret `I_update` for the group member
|
||||
`ID`.
|
||||
The ratchet state is stored in `gamma.ratchet[ID]`.
|
||||
It is required to use a HMAC-based key derivation function HKDF to combine the
|
||||
ratchet state with an input, returning an update secret and a new ratchet
|
||||
state.
|
||||
|
||||
```text
|
||||
(updateSecret, gamma.ratchet[ID]) = HKDF(gamma.ratchet[ID], input)
|
||||
return (gamma, updateSecret)
|
||||
|
||||
```
|
||||
|
||||
#### member-view
|
||||
|
||||
This function calculates the set of group members
|
||||
based on the most recent control message sent by the specified user `ID`.
|
||||
It filters the group membership operations
|
||||
to include only those observed by the specified `ID`, and
|
||||
then invokes the DGM function to generate the group membership.
|
||||
|
||||
```text
|
||||
ops = {m in gamma.history st. m was sent or acknowledged by ID}
|
||||
return DGM(ops)
|
||||
|
||||
```
|
||||
|
||||
#### generate-seed
|
||||
|
||||
This functions generates a random bit string and
|
||||
sends it encrypted to each member of the group using the `2SM` mechanism.
|
||||
It returns the updated protocol state and
|
||||
the set of direct messages (denoted as `dmsgs`) to send.
|
||||
|
||||
```text
|
||||
gamma.nextSeed = random.randbytes()
|
||||
dmsgs = empty
|
||||
for each ID in recipients:
|
||||
(gamma, msg) = encrypt-to(gamma, ID, gamma.nextSeed)
|
||||
dmsgs = dmsgs + (ID, msg)
|
||||
return (gamma, dmsgs)
|
||||
|
||||
```
|
||||
|
||||
### Creation of a group
|
||||
|
||||
A group is generated in a 3 steps procedure:
|
||||
|
||||
1. A user calls the `create` function and broadcasts a control message of type
|
||||
*create*.
|
||||
2. Each receiver of the message processes the message and broadcasts an *ack*
|
||||
control message.
|
||||
3. Each member processes the *ack* message received.
|
||||
|
||||
#### create
|
||||
|
||||
This function generates a *create* control message and calls `generate-seed` to
|
||||
define the set of direct messages that need to be sent.
|
||||
Then it calls `process-create` to process the control message for this user.
|
||||
The function `process-create` returns a tuple including an updated state gamma
|
||||
and an update secret `I`.
|
||||
|
||||
```text
|
||||
control = (“create”, gamma.mySeq, IDs)
|
||||
(gamma, dmsgs) = generate-seed(gamma, IDs)
|
||||
(gamma, _, _, I, _) = process-create(gamma, gamma.myId, gamma.mySeq, IDs, empty_string)
|
||||
return (gamma, control, dmsgs, I)
|
||||
|
||||
```
|
||||
|
||||
#### process-seed
|
||||
|
||||
This function initially employs `member-view` to identify the users who were
|
||||
part of the group when the control message was dispatched.
|
||||
Then, it attempts to acquire the seed secret through the following steps:
|
||||
|
||||
1. If the control message was dispatched by the local user, it uses the most
|
||||
recent invocation of `generate-seed` stored the seed secret in
|
||||
`gamma.nextSeed`.
|
||||
2. If the `control` message was dispatched by another user, and the local user
|
||||
is among its recipients, the function utilizes `decrypt-from` to decrypt the
|
||||
direct message that includes the seed secret.
|
||||
3. Otherwise, it returns an `ack` message without deriving an update secret.
|
||||
|
||||
Afterwards, `process-seed` generates separate member secrets for each group
|
||||
member from the seed secret by combining the seed secret and
|
||||
each user ID using HKDF.
|
||||
The secret for the sender of the message is stored in `senderSecret`, while
|
||||
those for the other group members are stored in `gamma.memberSecret`.
|
||||
The sender's member secret is immediately utilized to update their KDF ratchet
|
||||
and compute their update secret `I_sender` using `update-ratchet`.
|
||||
If the local user is the sender of the control message, the process is
|
||||
completed, and the update secret is returned.
|
||||
However, if the seed secret is received from another user, an `ack` control
|
||||
message is constructed for broadcast, including the sender ID and sequence
|
||||
number of the message being acknowledged.
|
||||
|
||||
The final step computes an update secret `I_me` for the local user invoking the
|
||||
`process-ack` function.
|
||||
|
||||
```text
|
||||
recipients = member-view(gamma, sender) - {sender}
|
||||
if sender = gamma.myId then seed = gamma.nextSeed; gamma.nextSeed =
|
||||
empty_string
|
||||
else if gamma.myId in recipients then (gamma, seed) = decrypt-from(gamma,
|
||||
sender, dmsg)
|
||||
else
|
||||
return (gamma, (ack, ++gamma.mySeq, (sender, seq)), empty_string ,
|
||||
empty_string , empty_string)
|
||||
|
||||
for ID in recipients do gamma.memberSecret[sender, seq, ID] = HKDF(seed, ID)
|
||||
senderSecret = HKDF(seed, sender)
|
||||
(gamma, I_sender) = update-ratchet(gamma, sender, senderSecret)
|
||||
if sender = gamma.myId then return (gamma, empty_string , empty_string ,
|
||||
I_sender, empty_string)
|
||||
control = (ack, ++gamma.mySeq, (sender, seq))
|
||||
members = member-view(gamma, gamma.myId)
|
||||
forward = empty
|
||||
for ID in {members - (recipients + {sender})}
|
||||
s = gamma.memberSecret[sender, seq, gamma.myId]
|
||||
(gamma, msg) = encrypt-to(gamma, ID, s)
|
||||
forward = forward + {(ID, msg)}
|
||||
(gamma, _, _, I_me, _) = process-ack(gamma, gamma.myId, gamma.mySeq,
|
||||
(sender, seq), empty_string)
|
||||
return (gamma, control, forward, I_sender, I_me)
|
||||
|
||||
```
|
||||
|
||||
#### process-create
|
||||
|
||||
This function is called by the sender and each of the receivers of the `create`
|
||||
control message.
|
||||
First, it records the information from the create message in the
|
||||
`gamma.history+ {op}`, which is used to track group membership changes. Then,
|
||||
it proceeds to call `process-seed`.
|
||||
|
||||
```text
|
||||
op = (”create”, sender, seq, IDs)
|
||||
gamma.history = gamma.history + {op}
|
||||
return (process-seed(gamma, sender, seq, dmsg))
|
||||
|
||||
```
|
||||
|
||||
#### process-ack
|
||||
|
||||
This function is called by those group members once they receive an ack
|
||||
message.
|
||||
In `process-ack`, `ackID` and `ackSeq` are the sender and sequence number of
|
||||
the acknowledged message.
|
||||
Firstly, if the acknowledged message is a group membership operation, it
|
||||
records the acknowledgement in `gamma.history`.
|
||||
|
||||
Following this, the function retrieves the relevant member secret from
|
||||
`gamma.memberSecret`, which was previously obtained from the seed secret
|
||||
contained in the acknowledged message.
|
||||
|
||||
Finally, it updates the ratchet for the sender of the `ack` and returns the
|
||||
resulting update secret.
|
||||
|
||||
```text
|
||||
if (ackID, ackSeq) was a create / add / remove then
|
||||
op = ("ack", sender, seq, ackID, ackSeq)
|
||||
gamma.history = gamma.history + {op}`
|
||||
s = gamma.memberSecret[ackID, ackSeq, sender]
|
||||
gamma.memberSecret[ackID, ackSeq, sender] = empty_string
|
||||
if (s = empty_string) & (dmsg = empty_string) then return (gamma, empty_string,
|
||||
empty_string, empty_string, empty_string)
|
||||
if (s = empty_string) then (gamma, s) = decrypt-from(gamma, sender, dmsg)
|
||||
(gamma, I) = update-ratchet(gamma, sender, s)
|
||||
return (gamma, empty_string, empty_string, I, empty_string)
|
||||
|
||||
```
|
||||
|
||||
The HKDF function MUST follow RFC 5869 using the hash function SHA256.
|
||||
|
||||
### Post-compromise security updates and group member removal
|
||||
|
||||
The functions `update` and `remove` share similarities with `create`:
|
||||
they both call the function `generate-seed` to encrypt a new seed secret for
|
||||
each group member.
|
||||
The distinction lies in the determination of the group members using `member
|
||||
view`.
|
||||
In the case of `remove`, the user being removed is excluded from the recipients
|
||||
of the seed secret.
|
||||
Additionally, the control message they construct is designated with type
|
||||
`update` or `remove` respectively.
|
||||
|
||||
Likewise, `process-update` and `process-remove` are akin to `process-create`.
|
||||
The function `process-update` skips the update of `gamma.history`,
|
||||
whereas `process-remove` includes a removal operation in the history.
|
||||
|
||||
#### update
|
||||
|
||||
```text
|
||||
control = ("update", ++gamma.mySeq, empty_string)
|
||||
recipients = member-view(gamma, gamma.myId) - {gamma.myId}
|
||||
(gamma, dmsgs) = generate-seed(gamma, recipients)
|
||||
(gamma, _, _, I , _) = process-update(gamma, gamma.myId, gamma.mySeq,
|
||||
empty_string, empty_string)
|
||||
return (gamma, control, dmsgs, I)
|
||||
|
||||
```
|
||||
|
||||
#### remove
|
||||
|
||||
```text
|
||||
control = ("remove", ++gamma.mySeq, empty)
|
||||
recipients = member-view(gamma, gamma.myId) - {ID, gamma.myId}
|
||||
(gamma, dmsgs) = generate-seed(gamma, recipients)
|
||||
(gamma, _, _, I , _) = process-update(gamma, gamma.myId, gamma.mySeq, ID,
|
||||
empty_string)
|
||||
return (gamma, control, dmsgs, I)
|
||||
|
||||
```
|
||||
|
||||
#### process-update
|
||||
|
||||
`return process-seed(gamma, sender, seq, dmsg)`
|
||||
|
||||
#### process-remove
|
||||
|
||||
```text
|
||||
op = ("remove", sender, seq, removed)
|
||||
gamma.history = gamma.history + {op}
|
||||
return process-seed(gamma, sender, seq, dmsg)
|
||||
|
||||
```
|
||||
|
||||
### Group member addition
|
||||
|
||||
#### add
|
||||
|
||||
When adding a new group member, an existing member initiates the process by
|
||||
invoking the `add` function and providing the ID of the user to be added.
|
||||
This function prepares a control message marked as `add` for broadcast to the
|
||||
group. Simultaneously, it creates a welcome message intended for the new member
|
||||
as a direct message.
|
||||
This `welcome` message includes the current state of the sender's KDF ratchet,
|
||||
encrypted using `2SM`, along with the history of group membership operations
|
||||
conducted so far.
|
||||
|
||||
```text
|
||||
control = ("add", ++gamma.mySeq, ID)
|
||||
(gamma, c) = encrypt-to(gamma, ID, gamma.ratchet[gamma.myId])
|
||||
op = ("add", gamma.myId, gamma.mySeq, ID)
|
||||
welcome = (gamma.history + {op}, c)
|
||||
(gamma, _, _, I, _) = process-add(gamma, gamma.myId, gamma.mySeq, ID, empty_string)
|
||||
return (gamma, control, (ID, welcome), I)
|
||||
|
||||
```
|
||||
|
||||
#### process-add
|
||||
|
||||
This function is invoked by both the sender and each recipient of an `add`
|
||||
message, which includes the new group member. If the local user is the newly
|
||||
added member, the function proceeds to call `process-welcome` and then exits.
|
||||
Otherwise, it extends `gamma.history` with the `add` operation.
|
||||
|
||||
Line 5 determines whether the local user was already a group member at the time
|
||||
the `add` message was sent; this condition is typically true but may be false
|
||||
if multiple users were added concurrently.
|
||||
|
||||
On lines 6 to 8, the ratchet for the sender of the *add* message is updated
|
||||
twice. In both calls to `update-ratchet`, a constant string is used as the
|
||||
ratchet input instead of a random seed secret.
|
||||
|
||||
The value returned by the first ratchet update is stored in
|
||||
`gamma.memberSecret` as the added user’s initial member secret. The result of
|
||||
the second ratchet update becomes `I_sender`, the update secret for the sender
|
||||
of the `add` message. On line 10, if the local user is the sender, the update
|
||||
secret is returned.
|
||||
|
||||
If the local user is not the sender, an acknowledgment for the `add` message is
|
||||
required.
|
||||
Therefore, on line 11, a control message of type `add-ack` is constructed for
|
||||
broadcast.
|
||||
Subsequently, in line 12 the current ratchet state is encrypted using `2SM` to
|
||||
generate a direct message intended for the added user, allowing them to decrypt
|
||||
subsequent messages sent by the sender.
|
||||
Finally, in lines 13 to 15, `process-add-ack` is called to calculate the local
|
||||
user’s update secret (`I_me`), which is then returned along with `I_sender`.
|
||||
|
||||
```text
|
||||
if added = gamma.myId then return process-welcome(gamma, sender, seq, dmsg)
|
||||
op = ("add", sender, seq, added)
|
||||
gamma.history = gamma.history + {op}
|
||||
if gamma.myId in member-view(gamma, sender) then
|
||||
(gamma, s) = update-ratchet(gamma, sender, "welcome")
|
||||
gamma.memberSecret[sender, seq, added] = s
|
||||
(gamma, I_sender) = update-ratchet(gamma, sender, "add")
|
||||
else I_sender = empty_string
|
||||
if sender = gamma.myId then return (gamma, empty_string, empty_string,
|
||||
I_sender, empty_string)
|
||||
control = ("add-ack", ++gamma.mySeq, (sender, seq))
|
||||
(gamma, c) = encrypt-to(gamma, added, ratchet[gamma.myId])
|
||||
(gamma, _, _, I_me, _) = process-add-ack(gamma, gamma.myId, gamma.mySeq,
|
||||
(sender, seq), empty_string)
|
||||
return (gamma, control, {(added, c)}, I_sender, I_me)
|
||||
|
||||
```
|
||||
|
||||
#### process-add-ack
|
||||
|
||||
This function is invoked by both the sender and each recipient of an `add-ack`
|
||||
message, including the new group member. Upon lines 1–2, the acknowledgment is
|
||||
added to `gamma.history`, mirroring the process in `process-ack`.
|
||||
If the current user is the new group member, the `add-ack` message includes the
|
||||
direct message constructed in `process-add`; this direct message contains the
|
||||
encrypted ratchet state of the sender of the `add-ack`, then it is decrypted on
|
||||
lines 3–5.
|
||||
|
||||
Upon line 6, a check is performed to check if the local user was already a
|
||||
group member at the time the `add-ack` was sent. If affirmative, a new update
|
||||
secret `I` for the sender of the `add-ack` is computed on line 7 by invoking
|
||||
`update-ratchet` with the constant string `add`.
|
||||
|
||||
In the scenario involving the new member, the ratchet state was recently
|
||||
initialized on line 5. This ratchet update facilitates all group members,
|
||||
including the new addition, to derive each member’s update by obtaining any
|
||||
update secret from before their inclusion.
|
||||
|
||||
```text
|
||||
op = ("ack", sender, seq, ackID, ackSeq)
|
||||
gamma$.history = gamma.history + {op}
|
||||
if dmsg != empty_string then
|
||||
(gamma, s) = decrypt-from(gamma, sender, dmsg)
|
||||
gamma.ratchet[sender] = s
|
||||
if gamma.myId in member-view(gamma, sender) then
|
||||
(gamma, I) = update-ratchet(gamma, sender, "add")
|
||||
return (gamma, empty_string, empty_string, I, empty_string)
|
||||
else return (gamma, empty_string, empty_string, empty_string, empty_string)
|
||||
|
||||
```
|
||||
|
||||
#### process-welcome
|
||||
|
||||
This function serves as the second step called by a newly added group member.
|
||||
In this context, `adderHistory` represents the adding user’s copy of
|
||||
`gamma.history` sent in their welcome message, which is utilized to initialize
|
||||
the added user’s history.
|
||||
Here, `c` denotes the ciphertext of the adding user’s ratchet state, which is
|
||||
decrypted on line 2 using `decrypt-from`.
|
||||
|
||||
Once `gamma.ratchet[sender]` is initialized, `update-ratchet` is invoked twice
|
||||
on lines 3 to 5 with the constant strings `welcome` and `add` respectively.
|
||||
These operations mirror the ratchet operations performed by every other group
|
||||
member in `process-add`.
|
||||
The outcome of the first `update-ratchet` call becomes the first member secret
|
||||
for the added user,
|
||||
while the second call returns `I_sender`, the update secret for the sender of
|
||||
the add operation.
|
||||
|
||||
Subsequently, the new group member constructs an *ack* control message to
|
||||
broadcast on line 6 and calls `process-ack` to compute their initial update
|
||||
secret I_me. The function `process-ack` reads from `gamma.memberSecret` and
|
||||
passes it to `update-ratchet`. The previous ratchet state for the new member is
|
||||
the empty string `empty`, as established by `init`, thereby initializing the
|
||||
new member’s ratchet.
|
||||
Upon receiving the new member’s `ack`, every other group member initializes
|
||||
their copy of the new member’s ratchet in a similar manner.
|
||||
|
||||
By the conclusion of `process-welcome`, the new group member has acquired
|
||||
update secrets for themselves and the user who added them.
|
||||
The ratchets for other group members are initialized by `process-add-ack`.
|
||||
|
||||
```text
|
||||
gamma.history = adderHistory
|
||||
(gamma, gamma.ratchet[sender]) = decrypt-from(gamma, sender, c)
|
||||
(gamma, s) = update-ratchet(gamma, sender, "welcome")
|
||||
gamma.memberSecret[sender, seq, gamma.myId] = s
|
||||
(gamma, I_sender) = update-ratchet(gamma, sender, "add")
|
||||
control = ("ack", ++gamma.mySeq, (sender, seq))
|
||||
(gamma, _, _, I_me, _) = process-ack(gamma, gamma.myId, gamma.mySeq, (sender,
|
||||
seq), empty_string)
|
||||
return (gamma, control, empty_string , I_sender, I_me)
|
||||
|
||||
```
|
||||
|
||||
## Privacy Considerations
|
||||
|
||||
### Dependency on PKI
|
||||
|
||||
The [DCGKA](https://eprint.iacr.org/2020/1281) proposal presents some
|
||||
limitations highlighted by the authors.
|
||||
Among these limitations one finds the requirement of a PKI (or a key server)
|
||||
mapping IDs to public keys.
|
||||
|
||||
One method to overcome this limitation is adapting the protocol SIWE (Sign in
|
||||
with Ethereum) so a user `u_1` who wants to start a communication with a user
|
||||
`u_2` can interact with latter’s wallet to request a public key using an
|
||||
Ethereum address as `ID`.
|
||||
|
||||
#### SIWE
|
||||
|
||||
The [SIWE](https://docs.login.xyz/general-information/siwe-overview) (Sign In
|
||||
With Ethereum) proposal was a suggested standard for leveraging Ethereum to
|
||||
authenticate and authorize users on web3 applications.
|
||||
Its goal is to establish a standardized method for users to sign in to web3
|
||||
applications using their Ethereum address and private key,
|
||||
mirroring the process by which users currently sign in to web2 applications
|
||||
using their email and password.
|
||||
Below follows the required steps:
|
||||
|
||||
1. A server generates a unique Nonce for each user intending to sign in.
|
||||
2. A user initiates a request to connect to a website using their wallet.
|
||||
3. The user is presented with a distinctive message that includes the Nonce and
|
||||
details about the website.
|
||||
4. The user authenticates their identity by signing in with their wallet.
|
||||
5. Upon successful authentication, the user's identity is confirmed or
|
||||
approved.
|
||||
6. The website grants access to data specific to the authenticated user.
|
||||
|
||||
#### Our approach
|
||||
|
||||
The idea in the [DCGKA](https://eprint.iacr.org/2020/1281) setting closely
|
||||
resembles the procedure outlined in SIWE. Here:
|
||||
|
||||
1. The server corresponds to user D1,who initiates a request (instead of
|
||||
generating a nonce) to obtain the public key of user D2.
|
||||
2. Upon receiving the request, the wallet of D2 send the request to the user,
|
||||
3. User D2 receives the request from the wallet, and decides whether accepts or
|
||||
rejects.
|
||||
4. The wallet and responds with a message containing the requested public key
|
||||
in case of acceptance by D2.
|
||||
|
||||
This message may be signed, allowing D1 to verify that the owner of the
|
||||
received public key is indeed D2.
|
||||
|
||||
### Multi-device setting
|
||||
|
||||
One may see the set of devices as a group and create a group key for internal
|
||||
communications.
|
||||
One may use treeKEM for instance, since it provides interesting properties like
|
||||
forward secrecy and post-compromise security.
|
||||
All devices share the same `ID`, which is held by one of them, and from other
|
||||
user’s point of view, they would look as a single user.
|
||||
|
||||
Using servers, like in the paper
|
||||
[Multi-Device for Signal](https://eprint.iacr.org/2019/1363), should be
|
||||
avoided; but this would imply using a particular device as receiver and
|
||||
broadcaster within the group.
|
||||
There is an obvious drawback which is having a single device working as a
|
||||
“server”. Should this device be attacked or without connection, there should be
|
||||
a mechanism for its revocation and replacement.
|
||||
|
||||
Another approach for communications between devices could be using the keypair
|
||||
of each device. This could open the door to use UPKE, since keypairs should be
|
||||
regenerated frequently.
|
||||
|
||||
Each time a device sends a message, either an internal message or an external
|
||||
message, it needs to replicate and broadcast it to all devices in the group.
|
||||
|
||||
The mechanism for the substitution of misbehaving leader devices follows:
|
||||
|
||||
1. Each device within a group knows the details of other leader devices. This
|
||||
information may come from metadata in received messages, and is replicated by
|
||||
the leader device.
|
||||
2. To replace a leader, the user should select any other device within its
|
||||
group and use it to send a signed message to all other users.
|
||||
3. To get the ability to sign messages, this new leader should request the
|
||||
keypair associated to the ID to the wallet.
|
||||
4. Once the leader has been changed, it revocates access from DCGKA to the
|
||||
former leader using the DCGKA protocol.
|
||||
5. The new leader starts a key update in DCGKA.
|
||||
|
||||
Not all devices in a group should be able to send messages to other users. Only
|
||||
the leader device should be in charge of sending and receiving messages.
|
||||
To prevent other devices from sending messages outside their group, a
|
||||
requirement should be signing each message. The keys associated to the `ID`
|
||||
should only be in control of the leader device.
|
||||
|
||||
The leader device is in charge of setting the keys involved in the DCGKA. This
|
||||
information must be replicated within the group to make sure it is updated.
|
||||
|
||||
To detect missing messages or potential misbehavior, messages must include a
|
||||
counter.
|
||||
|
||||
### Using UPKE
|
||||
|
||||
Managing the group of devices of a user can be done either using a group key
|
||||
protocol such as treeKEM or using the keypair of each device.
|
||||
Setting a common key for a group of devices under the control of the same actor
|
||||
might be excessive, furthermore it may imply some of the problems one can find
|
||||
in the usual setting of a group of different users;
|
||||
for example: one of the devices may not participate in the required updating
|
||||
processes, representing a threat for the group.
|
||||
|
||||
The other approach to managing the group of devices is using each device’s
|
||||
keypair, but it would require each device updating these materia frequently,
|
||||
something that may not happens.
|
||||
|
||||
[UPKE](https://eprint.iacr.org/2022/068) is a form of asymetric cryptography
|
||||
where any user can update any other user’s key pair by running an update
|
||||
algorithm with (high-entropy) private coins. Any sender can initiate a *key
|
||||
update* by sending a special update ciphertext.
|
||||
This ciphertext updates the receiver’s public key and also, once processed by
|
||||
the receiver, will update their secret key.
|
||||
|
||||
To the best of my knowledge, there exists several efficient constructions both
|
||||
[UPKE from ElGamal](https://eprint.iacr.org/2019/1189) (based in the DH
|
||||
assumption) and [UPKE from Lattices]((https://eprint.iacr.org/2023/1400))
|
||||
(based in lattices).
|
||||
None of them have been implemented in a secure messaging protocol, and this
|
||||
opens the door to some novel research.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright and related rights waived via
|
||||
[CC0](https://creativecommons.org/publicdomain/zero/1.0/).
|
||||
|
||||
## References
|
||||
|
||||
- [DCGKA](https://eprint.iacr.org/2020/1281)
|
||||
- [MLS](https://messaginglayersecurity.rocks)
|
||||
- [CoCoa](https://eprint.iacr.org/2022/251)
|
||||
- [Causal TreeKEM](https://mattweidner.com/assets/pdf/acs-dissertation.pdf)
|
||||
- [SIWE](https://docs.login.xyz/general-information/siwe-overview)
|
||||
- [Multi-device for Signal](https://eprint.iacr.org/2019/1363)
|
||||
- [UPKE](https://eprint.iacr.org/2022/068)
|
||||
- [UPKE from ElGamal](https://eprint.iacr.org/2019/1189)
|
||||
- [UPKE from Lattices](https://eprint.iacr.org/2023/1400)
|
||||
@@ -4,8 +4,8 @@ name: Secure channel setup using decentralized MLS and Ethereum accounts
|
||||
status: raw
|
||||
category: Standards Track
|
||||
tags:
|
||||
editor: Ramses Fernandez <ramses@status.im>
|
||||
contributors: Aaryamann Challani <aaryamann@status.im>, Ekaterina Broslavskaya <ekaterina@status.im>, Ugur Sen <ugur@status.im>, Ksr <ksr@status.im>
|
||||
editor: Ugur Sen <ugur@status.im>
|
||||
contributors: Ekaterina Broslavskaya <ekaterina@status.im>, Ksr <ksr@status.im>
|
||||
---
|
||||
|
||||
## Motivation
|
||||
@@ -26,8 +26,6 @@ The specification is divided into the following sections:
|
||||
|
||||
- Private group messaging protocol, based on the
|
||||
[MLS protocol](https://datatracker.ietf.org/doc/rfc9420/).
|
||||
- Specification of an Ethereum-based authentication protocol, based on
|
||||
[SIWE](https://eips.ethereum.org/EIPS/eip-4361).
|
||||
|
||||
## Protocol flow
|
||||
|
||||
@@ -80,13 +78,6 @@ ensuring message integrity.
|
||||
Group members use the current ``GroupContext`` to validate incoming messages
|
||||
and ensure they are consistent with the current group state.
|
||||
|
||||
### Use of smart contracts
|
||||
|
||||
This protocol accomplishes decentralization
|
||||
through the use of smart contracts for managing groups.
|
||||
They are used to register users in a group and keep the state of the group updated.
|
||||
Smart contracts MUST include an ACL to keep the state of the group.
|
||||
|
||||
## Private group messaging protocol
|
||||
|
||||
### Background
|
||||
@@ -673,308 +664,6 @@ communicating the update to all users.
|
||||
|
||||

|
||||
|
||||
## Ethereum-based authentication protocol
|
||||
|
||||
### Introduction
|
||||
|
||||
Sign-in with Ethereum describes how Ethereum accounts authenticate with
|
||||
off-chain services by signing a standard message format
|
||||
parameterized by scope, session details, and security mechanisms.
|
||||
Sign-in with Ethereum (SIWE), which is described in the
|
||||
[EIP 4361](https://eips.ethereum.org/EIPS/eip-4361),
|
||||
MUST be the authentication method required.
|
||||
|
||||
### Pattern
|
||||
|
||||
#### Message format (ABNF)
|
||||
|
||||
A SIWE Message MUST conform with the following Augmented Backus–Naur
|
||||
Form ([RFC 5234](https://datatracker.ietf.org/doc/html/rfc5234))
|
||||
expression.
|
||||
|
||||
```text
|
||||
sign-in-with-ethereum =
|
||||
[ scheme "://" ] domain %s" wants you to sign in with your
|
||||
Ethereum account:" LF address LF
|
||||
LF
|
||||
[ statement LF ]
|
||||
LF
|
||||
%s"URI: " uri LF
|
||||
%s"Version: " version LF
|
||||
%s"Chain ID: " chain-id LF
|
||||
%s"Nonce: " nonce LF
|
||||
%s"Issued At: " issued-at
|
||||
[ LF %s"Expiration Time: " expiration-time ]
|
||||
[ LF %s"Not Before: " not-before ]
|
||||
[ LF %s"Request ID: " request-id ]
|
||||
[ LF %s"Resources:"
|
||||
resources ]
|
||||
|
||||
scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
; See RFC 3986 for the fully contextualized
|
||||
; definition of "scheme".
|
||||
|
||||
domain = authority
|
||||
; From RFC 3986:
|
||||
; authority = [ userinfo "@" ] host [ ":" port ]
|
||||
; See RFC 3986 for the fully contextualized
|
||||
; definition of "authority".
|
||||
|
||||
address = "0x" 40*40HEXDIG
|
||||
; Must also conform to captilization
|
||||
; checksum encoding specified in EIP-55
|
||||
; where applicable (EOAs).
|
||||
|
||||
statement = *( reserved / unreserved / " " )
|
||||
; See RFC 3986 for the definition
|
||||
; of "reserved" and "unreserved".
|
||||
; The purpose is to exclude LF (line break).
|
||||
|
||||
uri = URI
|
||||
; See RFC 3986 for the definition of "URI".
|
||||
|
||||
version = "1"
|
||||
|
||||
chain-id = 1*DIGIT
|
||||
; See EIP-155 for valid CHAIN_IDs.
|
||||
|
||||
nonce = 8*( ALPHA / DIGIT )
|
||||
; See RFC 5234 for the definition
|
||||
; of "ALPHA" and "DIGIT".
|
||||
|
||||
issued-at = date-time
|
||||
expiration-time = date-time
|
||||
not-before = date-time
|
||||
; See RFC 3339 (ISO 8601) for the
|
||||
; definition of "date-time".
|
||||
|
||||
request-id = *pchar
|
||||
; See RFC 3986 for the definition of "pchar".
|
||||
|
||||
resources = *( LF resource )
|
||||
|
||||
resource = "- " URI
|
||||
```
|
||||
|
||||
This specification defines the following SIWE Message fields that can
|
||||
be parsed from a SIWE Message by following the rules in
|
||||
ABNF Message Format.
|
||||
This section follows the section _ABNF message format_
|
||||
in [EIP 4361](https://eips.ethereum.org/EIPS/eip-4361).
|
||||
|
||||
- `scheme` OPTIONAL. The URI scheme of the origin of the request.
|
||||
Its value MUST be a
|
||||
[RFC 3986](https://datatracker.ietf.org/doc/htmlrfc3986)
|
||||
URI scheme.
|
||||
|
||||
- `domain` REQUIRED.
|
||||
The domain that is requesting the signing.
|
||||
Its value MUST be a [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986)
|
||||
authority. The authority includes an OPTIONAL port.
|
||||
If the port is not specified, the default
|
||||
port for the provided scheme is assumed.
|
||||
|
||||
If scheme is not specified, HTTPS is assumed by default.
|
||||
|
||||
- `address` REQUIRED. The Ethereum address performing the signing.
|
||||
Its value SHOULD be conformant to mixed-case checksum address encoding
|
||||
specified in ERC-55 where applicable.
|
||||
|
||||
- `statement` OPTIONAL. A human-readable ASCII assertion that the user
|
||||
will sign which MUST NOT include '\n' (the byte 0x0a).
|
||||
- `uri` REQUIRED. An
|
||||
[RFC 3986](https://datatracker.ietf.org/doc/htmlrfc3986)
|
||||
URI referring to the resource that is the subject of the
|
||||
signing.
|
||||
|
||||
- `version` REQUIRED. The current version of the SIWE Message, which
|
||||
MUST be 1 for this specification.
|
||||
|
||||
- `chain-id` REQUIRED. The EIP-155 Chain ID to which the session is
|
||||
bound, and the network where Contract Accounts MUST be resolved.
|
||||
|
||||
- `nonce` REQUIRED. A random string (minimum 8 alphanumeric characters)
|
||||
chosen by the relying party and used to prevent replay attacks.
|
||||
|
||||
- `issued-at` REQUIRED. The time when the message was generated,
|
||||
typically the current time.
|
||||
|
||||
Its value MUST be an ISO 8601 datetime string.
|
||||
|
||||
- `expiration-time` OPTIONAL. The time when the signed authentication
|
||||
message is no longer valid.
|
||||
|
||||
Its value MUST be an ISO 8601 datetime string.
|
||||
|
||||
- `not-before` OPTIONAL. The time when the signed authentication
|
||||
message will become valid.
|
||||
|
||||
Its value MUST be an ISO 8601 datetime string.
|
||||
|
||||
- `request-id` OPTIONAL. An system-specific identifier that MAY be used
|
||||
to uniquely refer to the sign-in request.
|
||||
|
||||
- `resources` OPTIONAL. A list of information or references to
|
||||
information the user wishes to have resolved as part of authentication
|
||||
by the relying party.
|
||||
|
||||
Every resource MUST be a RFC 3986 URI separated by "\n- " where \n is
|
||||
the byte 0x0a.
|
||||
|
||||
#### Signing and Verifying Messages with Ethereum Accounts
|
||||
|
||||
- For Externally Owned Accounts, the verification method specified in
|
||||
[ERC-191](https://eips.ethereum.org/EIPS/eip-191)
|
||||
MUST be used.
|
||||
|
||||
- For Contract Accounts,
|
||||
|
||||
- The verification method specified in
|
||||
[ERC-1271](https://eips.ethereum.org/EIPS/eip-1271)
|
||||
SHOULD be used.
|
||||
Otherwise, the implementer MUST clearly define the
|
||||
verification method to attain security and interoperability
|
||||
for both wallets and relying parties.
|
||||
|
||||
- When performing [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271)
|
||||
signature verification, the contract performing the verification MUST
|
||||
be resolved from the specified `chain-id`.
|
||||
|
||||
- Implementers SHOULD take into consideration that
|
||||
[ERC-1271](https://eips.ethereum.org/EIPS/eip-1271)
|
||||
implementations are not required to be pure functions.
|
||||
They can return different results for the same inputs depending
|
||||
on blockchain state.
|
||||
This can affect the security model and session validation rules.
|
||||
|
||||
#### Resolving Ethereum Name Service (ENS) Data
|
||||
|
||||
- The relying party or wallet MAY additionally perform resolution of
|
||||
ENS data, as this can improve the user experience by displaying human
|
||||
friendly information that is related to the `address`.
|
||||
Resolvable ENS data include:
|
||||
|
||||
- The primary ENS name.
|
||||
- The ENS avatar.
|
||||
- Any other resolvable resources specified in the ENS documentation.
|
||||
|
||||
- If resolution of ENS data is performed, implementers SHOULD take
|
||||
precautions to preserve user privacy and consent.
|
||||
Their `address` could be forwarded to third party services as part of
|
||||
the resolution process.
|
||||
|
||||
#### Implementer steps: specifying the request origin
|
||||
|
||||
The `domain` and, if present, the `scheme`, in the SIWE Message MUST
|
||||
correspond to the origin from where the signing request was made.
|
||||
|
||||
#### Implementer steps: verifying a signed message
|
||||
|
||||
The SIWE Message MUST be checked for conformance to the ABNF Message
|
||||
Format and its signature MUST be checked as defined in Signing and
|
||||
Verifying Messages with Ethereum Accounts.
|
||||
|
||||
#### Implementer steps: creating sessions
|
||||
|
||||
Sessions MUST be bound to the address and not to further resolved
|
||||
resources that can change.
|
||||
|
||||
#### Implementer steps: interpreting and resolving resources
|
||||
|
||||
Implementers SHOULD ensure that that URIs in the listed resources are
|
||||
human-friendly when expressed in plaintext form.
|
||||
|
||||
#### Wallet implementer steps: verifying the message format
|
||||
|
||||
The full SIWE message MUST be checked for conformance to the ABNF
|
||||
defined in ABNF Message Format.
|
||||
|
||||
Wallet implementers SHOULD warn users if the substring `"wants you to
|
||||
sign in with your Ethereum account"` appears anywhere in an
|
||||
[ERC-191](https://eips.ethereum.org/EIPS/eip-191)
|
||||
message signing request unless the message fully conforms to the format
|
||||
defined ABNF Message Format.
|
||||
|
||||
#### Wallet implementer steps: verifying the request origin
|
||||
|
||||
Wallet implementers MUST prevent phishing attacks by verifying the
|
||||
origin of the request against the `scheme` and `domain` fields in the
|
||||
SIWE Message.
|
||||
|
||||
The origin SHOULD be read from a trusted data source such as the
|
||||
browser window or over WalletConnect
|
||||
[ERC-1328](https://eips.ethereum.org/EIPS/eip-1328) sessions for
|
||||
comparison against the signing message contents.
|
||||
|
||||
Wallet implementers MAY warn instead of rejecting the verification if
|
||||
the origin is pointing to localhost.
|
||||
|
||||
The following is a RECOMMENDED algorithm for Wallets to conform with
|
||||
the requirements on request origin verification defined by
|
||||
this specification.
|
||||
|
||||
The algorithm takes the following input variables:
|
||||
|
||||
- fields from the SIWE message.
|
||||
- `origin` of the signing request: the origin of the page which
|
||||
requested the signin via the provider.
|
||||
- `allowedSchemes`: a list of schemes allowed by the Wallet.
|
||||
- `defaultScheme`: a scheme to assume when none was provided. Wallet
|
||||
implementers in the browser SHOULD use https.
|
||||
- developer mode indication: a setting deciding if certain risks should
|
||||
be a warning instead of rejection. Can be manually configured or
|
||||
derived from `origin` being localhost.
|
||||
|
||||
The algorithm is described as follows:
|
||||
|
||||
- If `scheme` was not provided, then assign `defaultScheme` as scheme.
|
||||
- If `scheme` is not contained in `allowedSchemes`, then the `scheme`
|
||||
is not expected and the Wallet MUST reject the request.
|
||||
Wallet implementers in the browser SHOULD limit the list of
|
||||
allowedSchemes to just 'https' unless a developer mode is activated.
|
||||
- If `scheme` does not match the scheme of origin, the Wallet SHOULD
|
||||
reject the request.
|
||||
Wallet implementers MAY show a warning instead of rejecting the request
|
||||
if a developer mode is activated.
|
||||
In that case the Wallet continues processing the request.
|
||||
- If the `host` part of the `domain` and `origin` do not match, the
|
||||
Wallet MUST reject the request unless the Wallet is in developer mode.
|
||||
In developer mode the Wallet MAY show a warning instead and continues
|
||||
procesing the request.
|
||||
- If `domain` and `origin` have mismatching subdomains, the Wallet
|
||||
SHOULD reject the request unless the Wallet is in developer mode.
|
||||
In developer mode the Wallet MAY show a warning instead and continues
|
||||
procesing the request.
|
||||
- Let `port` be the port component of `domain`, and if no port is
|
||||
contained in domain, assign port the default port specified for the
|
||||
scheme.
|
||||
- If `port` is not empty, then the Wallet SHOULD show a warning if the
|
||||
`port` does not match the port of `origin`.
|
||||
- If `port` is empty, then the Wallet MAY show a warning if `origin`
|
||||
contains a specific port.
|
||||
- Return request origin verification completed.
|
||||
|
||||
#### Wallet implementer steps: creating SIWE interfaces
|
||||
|
||||
Wallet implementers MUST display to the user the following fields from
|
||||
the SIWE Message request by default and prior to signing, if they are
|
||||
present: `scheme`, `domain`, `address`, `statement`, and `resources`.
|
||||
Other present fields MUST also be made available to the user prior to
|
||||
signing either by default or through an extended interface.
|
||||
|
||||
Wallet implementers displaying a plaintext SIWE Message to the user
|
||||
SHOULD require the user to scroll to the bottom of the text area
|
||||
prior to signing.
|
||||
|
||||
Wallet implementers MAY construct a custom SIWE user interface by
|
||||
parsing the ABNF terms into data elements for use in the interface.
|
||||
The display rules above still apply to custom interfaces.
|
||||
|
||||
#### Wallet implementer steps: supporting internationalization (i18n)
|
||||
|
||||
After successfully parsing the message into ABNF terms, translation MAY
|
||||
happen at the UX level per human language.
|
||||
|
||||
## Consideration to secure 1-to-1 channels
|
||||
|
||||
There are situations where users need to set one-to-one communication
|
||||
@@ -1002,7 +691,7 @@ Concerning keys, each node can generate and disseminate their
|
||||
encryption key among the other nodes, so they can create a local
|
||||
version of the tree that allows for the generation of the group key.
|
||||
|
||||
Another important component is the _authentication service_, which is
|
||||
Another important component is the authentication service, which is
|
||||
replaced with SIWE in this specification.
|
||||
|
||||
## Privacy and Security Considerations
|
||||
@@ -1026,7 +715,6 @@ Copyright and related rights waived via [CC0](https://creativecommons.org/public
|
||||
- [Hybrid Public Key Encryption](https://datatracker.ietf.org/doc/rfc9180/)
|
||||
- [Security Analysis and Improvements for the IETF MLS Standard for Group Messaging](https://eprint.iacr.org/2019/1189.pdf)
|
||||
- [Signed Data Standard](https://eips.ethereum.org/EIPS/eip-191)
|
||||
- [Sign-In with Ethereum](https://eips.ethereum.org/EIPS/eip-4361)
|
||||
- [Standard Signature Validation Method for Contracts](https://eips.ethereum.org/EIPS/eip-1271)
|
||||
- [The Messaging Layer Security Protocol](https://datatracker.ietf.org/doc/rfc9420/)
|
||||
- [Toy Ethereum Private Messaging Protocol](https://rfc.vac.dev/spec/20/)
|
||||
|
||||
1364
vac/raw/eth-secpm.md
1364
vac/raw/eth-secpm.md
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
||||
---
|
||||
title: ETH-SECURE-CHANNEL
|
||||
name: Secure 1-to-1 channel setup using X3DH and the double ratchet
|
||||
status: raw
|
||||
category: Standards Track
|
||||
tags:
|
||||
editor: Ramses Fernandez <ramses@status.im>
|
||||
contributors:
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
The need for secure communications has become paramount.
|
||||
This specification outlines a protocol describing a
|
||||
secure 1-to-1 comunication channel between 2 users. The
|
||||
main components are the X3DH key establishment mechanism,
|
||||
combined with the double ratchet. The aim of this
|
||||
combination of schemes is providing a protocol with both
|
||||
forward secrecy and post-compromise security.
|
||||
|
||||
## Theory
|
||||
|
||||
The specification is based on the noise protocol framework.
|
||||
It corresponds to the double ratchet scheme combined with
|
||||
the X3DH algorithm, which will be used to initialize the former.
|
||||
We chose to express the protocol in noise to be be able to use
|
||||
the noise streamlined implementation and proving features.
|
||||
The X3DH algorithm provides both authentication and forward
|
||||
secrecy, as stated in the
|
||||
[X3DH specification](https://signal.org/docs/specifications/x3dh/).
|
||||
|
||||
This protocol will consist of several stages:
|
||||
|
||||
1. Key setting for X3DH: this step will produce
|
||||
prekey bundles for Bob which will be fed into X3DH.
|
||||
It will also allow Alice to generate the keys required
|
||||
to run the X3DH algorithm correctly.
|
||||
2. Execution of X3DH: This step will output
|
||||
a common secret key `SK` together with an additional
|
||||
data vector `AD`. Both will be used in the double
|
||||
ratchet algorithm initialization.
|
||||
3. Execution of the double ratchet algorithm
|
||||
for forward secure, authenticated communications,
|
||||
using the common secret key `SK`, obtained from X3DH, as a root key.
|
||||
|
||||
The protocol assumes the following requirements:
|
||||
|
||||
- Alice knows Bob’s Ethereum address.
|
||||
- Bob is willing to participate in the protocol,
|
||||
and publishes his public key.
|
||||
- Bob’s ownership of his public key is verifiable,
|
||||
- Alice wants to send message M to Bob.
|
||||
- An eavesdropper cannot read M’s content
|
||||
even if she is storing it or relaying it.
|
||||
|
||||
## Syntax
|
||||
|
||||
### Cryptographic suite
|
||||
|
||||
The following cryptographic functions MUST be used:
|
||||
|
||||
- `X488` as Diffie-Hellman function `DH`.
|
||||
- `SHA256` as KDF.
|
||||
- `AES256-GCM` as AEAD algorithm.
|
||||
- `SHA512` as hash function.
|
||||
- `XEd448` for digital signatures.
|
||||
|
||||
### X3DH initialization
|
||||
|
||||
This scheme MUST work on the curve curve448.
|
||||
The X3DH algorithm corresponds to the IX pattern in Noise.
|
||||
|
||||
Bob and Alice MUST define personal key pairs
|
||||
`(ik_B, IK_B)` and `(ik_A, IK_A)` respectively where:
|
||||
|
||||
- The key `ik` must be kept secret,
|
||||
- and the key `IK` is public.
|
||||
|
||||
Bob MUST generate new keys using
|
||||
`(ik_B, IK_B) = GENERATE_KEYPAIR(curve = curve448)`.
|
||||
|
||||
Bob MUST also generate a public key pair
|
||||
`(spk_B, SPK_B) = GENERATE_KEYPAIR(curve = curve448)`.
|
||||
|
||||
`SPK` is a public key generated and stored at medium-term.
|
||||
Both signed prekey and the certificate MUST
|
||||
undergo periodic replacement.
|
||||
After replacing the key,
|
||||
Bob keeps the old private key of `SPK`
|
||||
for some interval, dependant on the implementation.
|
||||
This allows Bob to decrypt delayed messages.
|
||||
|
||||
Bob MUST sign `SPK` for authentication:
|
||||
`SigSPK = XEd448(ik, Encode(SPK))`
|
||||
|
||||
A final step requires the definition of
|
||||
`prekey_bundle = (IK, SPK, SigSPK, OPK_i)`
|
||||
|
||||
One-time keys `OPK` MUST be generated as
|
||||
`(opk_B, OPK_B) = GENERATE_KEYPAIR(curve = curve448)`.
|
||||
|
||||
Before sending an initial message to Bob,
|
||||
Alice MUST generate an AD: `AD = Encode(IK_A) || Encode(IK_B)`.
|
||||
|
||||
Alice MUST generate ephemeral key pairs
|
||||
`(ek, EK) = GENERATE_KEYPAIR(curve = curve448)`.
|
||||
|
||||
The function `Encode()` transforms a
|
||||
curve448 public key into a byte sequence.
|
||||
This is specified in the [RFC 7748](http://www.ietf.org/rfc/rfc7748.txt)
|
||||
on elliptic curves for security.
|
||||
|
||||
One MUST consider `q = 2^446 - 13818066809895115352007386748515426880336692474882178609894547503885`
|
||||
for digital signatures with `(XEd448_sign, XEd448_verify)`:
|
||||
|
||||
```text
|
||||
XEd448_sign((ik, IK), message):
|
||||
Z = randbytes(64)
|
||||
r = SHA512(2^456 - 2 || ik || message || Z )
|
||||
R = (r * convert_mont(5)) % q
|
||||
h = SHA512(R || IK || M)
|
||||
s = (r + h * ik) % q
|
||||
return (R || s)
|
||||
```
|
||||
|
||||
```text
|
||||
XEd448_verify(u, message, (R || s)):
|
||||
if (R.y >= 2^448) or (s >= 2^446): return FALSE
|
||||
h = (SHA512(R || 156326 || message)) % q
|
||||
R_check = s * convert_mont(5) - h * 156326
|
||||
if R == R_check: return TRUE
|
||||
return FALSE
|
||||
```
|
||||
|
||||
```text
|
||||
convert_mont(u):
|
||||
u_masked = u % mod 2^448
|
||||
inv = ((1 - u_masked)^(2^448 - 2^224 - 3)) % (2^448 - 2^224 - 1)
|
||||
P.y = ((1 + u_masked) * inv)) % (2^448 - 2^224 - 1)
|
||||
P.s = 0
|
||||
return P
|
||||
```
|
||||
|
||||
### Use of X3DH
|
||||
|
||||
This specification combines the double ratchet
|
||||
with X3DH using the following data as initialization for the former:
|
||||
|
||||
- The `SK` output from X3DH becomes the `SK`
|
||||
input of the double ratchet. See section 3.3 of
|
||||
[Signal Specification](https://signal.org/docs/specifications/doubleratchet/)
|
||||
for a detailed description.
|
||||
- The `AD` output from X3DH becomes the `AD`
|
||||
input of the double ratchet. See sections 3.4 and 3.5 of
|
||||
[Signal Specification](https://signal.org/docs/specifications/doubleratchet/)
|
||||
for a detailed description.
|
||||
- Bob’s signed prekey `SigSPKB` from X3DH is used as Bob’s
|
||||
initial ratchet public key of the double ratchet.
|
||||
|
||||
X3DH has three phases:
|
||||
|
||||
1. Bob publishes his identity key and prekeys to a server,
|
||||
a network, or dedicated smart contract.
|
||||
2. Alice fetches a prekey bundle from the server,
|
||||
and uses it to send an initial message to Bob.
|
||||
3. Bob receives and processes Alice's initial message.
|
||||
|
||||
Alice MUST perform the following computations:
|
||||
|
||||
```text
|
||||
dh1 = DH(IK_A, SPK_B, curve = curve448)
|
||||
dh2 = DH(EK_A, IK_B, curve = curve448)
|
||||
dh3 = DH(EK_A, SPK_B)
|
||||
SK = KDF(dh1 || dh2 || dh3)
|
||||
```
|
||||
|
||||
Alice MUST send to Bob a message containing:
|
||||
|
||||
- `IK_A, EK_A`.
|
||||
- An identifier to Bob's prekeys used.
|
||||
- A message encrypted with AES256-GCM using `AD` and `SK`.
|
||||
|
||||
Upon reception of the initial message, Bob MUST:
|
||||
|
||||
1. Perform the same computations above with the `DH()` function.
|
||||
2. Derive `SK` and construct `AD`.
|
||||
3. Decrypt the initial message encrypted with `AES256-GCM`.
|
||||
4. If decryption fails, abort the protocol.
|
||||
|
||||
### Initialization of the double datchet
|
||||
|
||||
In this stage Bob and Alice have generated key pairs
|
||||
and agreed a shared secret `SK` using X3DH.
|
||||
|
||||
Alice calls `RatchetInitAlice()` defined below:
|
||||
|
||||
```text
|
||||
RatchetInitAlice(SK, IK_B):
|
||||
state.DHs = GENERATE_KEYPAIR(curve = curve448)
|
||||
state.DHr = IK_B
|
||||
state.RK, state.CKs = HKDF(SK, DH(state.DHs, state.DHr))
|
||||
state.CKr = None
|
||||
state.Ns, state.Nr, state.PN = 0
|
||||
state.MKSKIPPED = {}
|
||||
```
|
||||
|
||||
The HKDF function MUST be the proposal by
|
||||
[Krawczyk and Eronen](http://www.ietf.org/rfc/rfc5869.txt).
|
||||
In this proposal `chaining_key` and `input_key_material`
|
||||
MUST be replaced with `SK` and the output of `DH` respectively.
|
||||
|
||||
Similarly, Bob calls the function `RatchetInitBob()` defined below:
|
||||
|
||||
```text
|
||||
RatchetInitBob(SK, (ik_B,IK_B)):
|
||||
state.DHs = (ik_B, IK_B)
|
||||
state.Dhr = None
|
||||
state.RK = SK
|
||||
state.CKs, state.CKr = None
|
||||
state.Ns, state.Nr, state.PN = 0
|
||||
state.MKSKIPPED = {}
|
||||
```
|
||||
|
||||
### Encryption
|
||||
|
||||
This function performs the symmetric key ratchet.
|
||||
|
||||
```text
|
||||
RatchetEncrypt(state, plaintext, AD):
|
||||
state.CKs, mk = HMAC-SHA256(state.CKs)
|
||||
header = HEADER(state.DHs, state.PN, state.Ns)
|
||||
state.Ns = state.Ns + 1
|
||||
return header, AES256-GCM_Enc(mk, plaintext, AD || header)
|
||||
```
|
||||
|
||||
The `HEADER` function creates a new message header
|
||||
containing the public key from the key pair output of the `DH`function.
|
||||
It outputs the previous chain length `pn`,
|
||||
and the message number `n`.
|
||||
The returned header object contains ratchet public key
|
||||
`dh` and integers `pn` and `n`.
|
||||
|
||||
### Decryption
|
||||
|
||||
The function `RatchetDecrypt()` decrypts incoming messages:
|
||||
|
||||
```text
|
||||
RatchetDecrypt(state, header, ciphertext, AD):
|
||||
plaintext = TrySkippedMessageKeys(state, header, ciphertext, AD)
|
||||
if plaintext != None:
|
||||
return plaintext
|
||||
if header.dh != state.DHr:
|
||||
SkipMessageKeys(state, header.pn)
|
||||
DHRatchet(state, header)
|
||||
SkipMessageKeys(state, header.n)
|
||||
state.CKr, mk = HMAC-SHA256(state.CKr)
|
||||
state.Nr = state.Nr + 1
|
||||
return AES256-GCM_Dec(mk, ciphertext, AD || header)
|
||||
```
|
||||
|
||||
Auxiliary functions follow:
|
||||
|
||||
```text
|
||||
DHRatchet(state, header):
|
||||
state.PN = state.Ns
|
||||
state.Ns = state.Nr = 0
|
||||
state.DHr = header.dh
|
||||
state.RK, state.CKr = HKDF(state.RK, DH(state.DHs, state.DHr))
|
||||
state.DHs = GENERATE_KEYPAIR(curve = curve448)
|
||||
state.RK, state.CKs = HKDF(state.RK, DH(state.DHs, state.DHr))
|
||||
```
|
||||
|
||||
```text
|
||||
SkipMessageKeys(state, until):
|
||||
if state.NR + MAX_SKIP < until:
|
||||
raise Error
|
||||
if state.CKr != none:
|
||||
while state.Nr < until:
|
||||
state.CKr, mk = HMAC-SHA256(state.CKr)
|
||||
state.MKSKIPPED[state.DHr, state.Nr] = mk
|
||||
state.Nr = state.Nr + 1
|
||||
```
|
||||
|
||||
```text
|
||||
TrySkippedMessageKey(state, header, ciphertext, AD):
|
||||
if (header.dh, header.n) in state.MKSKIPPED:
|
||||
mk = state.MKSKIPPED[header.dh, header.n]
|
||||
delete state.MKSKIPPED[header.dh, header.n]
|
||||
return AES256-GCM_Dec(mk, ciphertext, AD || header)
|
||||
else: return None
|
||||
```
|
||||
|
||||
## Information retrieval
|
||||
|
||||
### Static data
|
||||
|
||||
Some data, such as the key pairs `(ik, IK)` for Alice and Bob,
|
||||
MAY NOT be regenerated after a period of time.
|
||||
Therefore the prekey bundle MAY be stored in long-term
|
||||
storage solutions, such as a dedicated smart contract
|
||||
which outputs such a key pair when receiving an Ethereum wallet
|
||||
address.
|
||||
|
||||
Storing static data is done using a dedicated
|
||||
smart contract `PublicKeyStorage` which associates
|
||||
the Ethereum wallet address of a user with his public key.
|
||||
This mapping is done by `PublicKeyStorage`
|
||||
using a `publicKeys` function, or a `setPublicKey` function.
|
||||
This mapping is done if the user passed an authorization process.
|
||||
A user who wants to retrieve a public key associated
|
||||
with a specific wallet address calls a function `getPublicKey`.
|
||||
The user provides the wallet address as the only
|
||||
input parameter for `getPublicKey`.
|
||||
The function outputs the associated public key
|
||||
from the smart contract.
|
||||
|
||||
### Ephemeral data
|
||||
|
||||
Storing ephemeral data on Ethereum MAY be done using
|
||||
a combination of on-chain and off-chain solutions.
|
||||
This approach provides an efficient solution to
|
||||
the problem of storing updatable data in Ethereum.
|
||||
|
||||
1. Ethereum stores a reference or a hash
|
||||
that points to the off-chain data.
|
||||
2. Off-chain solutions can include systems like IPFS,
|
||||
traditional cloud storage solutions, or
|
||||
decentralized storage networks such as a
|
||||
[Swarm](https://www.ethswarm.org).
|
||||
|
||||
In any case, the user stores the associated
|
||||
IPFS hash, URL or reference in Ethereum.
|
||||
|
||||
The fact of a user not updating the ephemeral information
|
||||
can be understood as Bob not willing to participate in any
|
||||
communication.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).
|
||||
|
||||
## References
|
||||
|
||||
- [The Double Ratchet Algorithm](https://signal.org/docs/specifications/doubleratchet/)
|
||||
- [The X3DH Key Agreement Protocol](https://signal.org/docs/specifications/x3dh/)
|
||||
Reference in New Issue
Block a user