Problem
Currently we rely on whisper for signing the messages.
It works by taking anything you send though whisper, signing the whole payload S(payload)
, and sending both payload and signature to other peers.
A client will use that signature and the payload to extract the public key of the sender, which we then use to:
- Identify which encryption key to use in case of PFS encrypted messages
- Identify the author of the message
This works fine for our use case, but has currently a few drawbacks:
- We rely on out-of-band information (whisper) for our own protocol (anything above whisper) for authentication
- It does not support relay messages to third parties, which is currently needed for MVDS, as currently it does not forward the whole whisper payload, but only the content of it, which means that current messages are unauthenticated.
It would also be beneficial for paired devices (whereby syncing anything would be just a matter of forwarding that message, effectively getting rid of the need of having a separate protocol and allowing to have more redundancy when a device has not been included, but that’s sci-fi programming for now).
Where we want to be
Ideally in the best case scenario, we would like our protocol to support both plausible deniability and relaying messages (the two things are not compatible), and let the client specify which one to use.
As it stands, messages are not plausibly deniable because of whisper (there’s a plausibly deniable mode in whisper, but it works by signing only the encryption key and not the content of the message, so not sure what kind of guarantees we can make).
How to get there
Public messages
Public messages needs to be signed, as there’s no such thing as a plausibly deniable public message (that would just be an anonymous message). They are also not encrypted (in our layer), so here the solution is pretty straightforward.
We can add to each message a signature of the raw payload (in our case of the transit encoding), so that we have:
bytes signature
bytes payload
Upon receiving a message a client will calculate the PK from the payload and the signature.
In this case we will not be using the whisper signature at all, therefore the message can be relayed to third parties, which will understand the original author of the message.
Backward compatibility
Backward compatibility is a bit tricky here, but it’s doable.
There are two cases that we need to consider:
- The message is not relayed, so the whisper key == key extracted from the signature. This is not a problem, old clients will use the whisper key, new clients will use the signature.
- The message is relayed, so the whisper key != key extracted from the signature. In this case, old clients will wrongly use the whisper key, resulting in the message being attributed to the relaying party and not the original author. To avoid this scenario, we can have the relayed message in a different field, and when relaying a message clients will leave the original
payload
empty. Effectively old clients will see an empty message, while new clients will understand it’s relayed message.
So the format of the message would be:
bytes signature
bytes payload // Empty for relayed messages
bytes relayedPayload // Empty for non-relayed messages
Another important property we would like is that we should be able to relay public messages over private channels.
For example:
Alice sends a message to the public chat #status
Bob receives the message
Bob & Donald do their dirty datasync stuff, and agree that Donald should be sent Alice message.
It is important that this message can be PFS encrypted. We will make sure that is the case once we explorer 1-to-1 communication.
One to one messages
One to one messages are a bit more complex, as we also need to correctly identify which encryption key we will use for decrypting the message (currently we use the key extracted by whisper).
Also, we have some negotiation over the protocol, so we just need to be careful not to make any assumptions about the content which we are encrypting/relaying and the peer that is sending it (i.e if the relaying node is running v2 , it does not mean that the message relayed is also running v2).
The crucial thing here is that we will not compute the signature over the encrypted payload, but always over the unencrypted payload. That’s because otherwise relaying would not work as the other peer might not be able to decrypt the same message. So the format would be:
bytes signature // Signature of the unencrypted payload
bytes payload // Empty for relayed messages
bytes relayedPayload // Empty for non-relayed messages
In this case we could also just have payload
instead of separating between relayedPayload
and payload
, as this are direct messages and we can just send only to those clients that we know will understand this, but we could keep it the same for consistency.
In order to be able to know which encryption key should be used to decrypt the message, we can simply add that to the payload:
bytes identity // The pk of the user sending the message
bytes signature // Signature of the unencrypted payload
bytes payload // Empty for relayed messages
bytes relayedPayload // Empty for non-relayed messages
So we can get rid of the dependency on whisper.
It is clear that this method works both for relaying publc chats or one to one messages (there’s no difference between them, as we always sign the unencrypted payload).
Plausible deniability
In plausible deniability mode, signature
will not be there.
In that case it can’t be a relayed message.
bytes identity
will still be there (but not signed, so anyone could “lie” about it)
We will use that identity to pull a double ratchet from the database, if the messages decrypts, we know bytes identity
has authenticated with us using X3DH
, so we have effectively validated identity
.
This will not work for initial non-PFS messages, which will still need to be signed, although those are increasingly rarer as we are getting better at propagating the bundle, but necessary until ux changes or swarm/ipfs integration.
If a signature is not present, that message will be plausibly deniable (not counting whisper), and we won’t be able to relay it.
Backward compatibility
This change is backward compatible
TLDR
We need to add in-protocol signature of messages to be able to use MVDS effectively and get rid of whisper dependency.
I propose changing the payload of messages to include two extra pieces of information:
For both public and one to one:
bytes signature
The signature of the unencrypted payload
For one to one:
bytes identity
The identity of the user sending the message (not of the user that authored the message)
The change is backward compatible by separating the payload
from the relayedPayload
:
bytes payload // Original payload, empty if relayed message bytes relayedPayload // Relayed payload, empty if not relayed
If we don’t want to be backward compatible (say v1 breaks), then we can simply have a single field:
bytes payload
And we know there’s not going to be any problem of wrong authentication.
What do people think? feedback is welcome