ETH-SECURE-CHANNEL
- Status: raw
- Category: Standards Track
- Editor: Ramses Fernandez <ramses@status.im>
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.
This protocol will consist of several stages:
- 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.
- Execution of X3DH: This step will output
a common secret key
SK
together with an additional data vectorAD
. Both will be used in the double ratchet algorithm initialization. - 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 functionDH
.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
on elliptic curves for security.
One MUST consider q = 2^446 - 13818066809895115352007386748515426880336692474882178609894547503885
for digital signatures with (XEd448_sign, XEd448_verify)
:
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)
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
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 theSK
input of the double ratchet. See section 3.3 of Signal Specification for a detailed description. - The
AD
output from X3DH becomes theAD
input of the double ratchet. See sections 3.4 and 3.5 of Signal Specification 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:
- Bob publishes his identity key and prekeys to a server, a network, or dedicated smart contract.
- Alice fetches a prekey bundle from the server, and uses it to send an initial message to Bob.
- Bob receives and processes Alice's initial message.
Alice MUST perform the following computations:
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
andSK
.
Upon reception of the initial message, Bob MUST:
- Perform the same computations above with the
DH()
function. - Derive
SK
and constructAD
. - Decrypt the initial message encrypted with
AES256-GCM
. - 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:
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.
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:
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.
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:
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:
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))
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
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.
- Ethereum stores a reference or a hash that points to the off-chain data.
- Off-chain solutions can include systems like IPFS, traditional cloud storage solutions, or decentralized storage networks such as a Swarm.
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.