Further separating the two specs by pointing to bolt08 for transport details and deduplicating some of the information. Also fixed some markup while I'm at it :-)
21 KiB
BOLT #8: Encrypted and Authenticated Transport
All communications between Lightning nodes is encrypted in order to
provide confidentiality for all transcripts between nodes, and authenticated to
avoid malicious interference. Each node has a known long-term identifier which
is a public key on Bitcoin's secp256k1
curve. This long-term public key is
used within the protocol to establish an encrypted+authenticated connection
with peers, and also to authenticate any information advertised on the behalf
of a node.
Cryptographic Messaging Overview
Prior to sending any lightning messages, nodes must first initiate the cryptographic session state which is used to encrypt and authenticate all messages sent between nodes. The initialization of this cryptographic session state is completely distinct from any inner protocol message header or conventions.
The transcript between two nodes is separated into two distinct segments:
- First, before any actual data transfer, both nodes participate in an authenticated key agreement protocol which is based off of the Noise Protocol Framework4.
- If the initial handshake is successful, then nodes enter the lightning
message exchange phase. In the lightning message exchange phase, all
messages are
AEAD
ciphertexts.
Authenticated Key Agreement Handshake
The handshake chosen for the authenticated key exchange is Noise_XK
. As a
"pre-message", we assume that the initiator knows the identity public key of
the responder. This handshake provides a degree of identity hiding for the
responder, its public key is never transmitted during the handshake. Instead,
authentication is achieved implicitly via a series of ECDH
(Elliptic-Curve
Diffie-Hellman) operations followed by a MAC
check.
The authenticated key agreement (Noise_XK
) is performed in three distinct
steps. During each "act" of the handshake, some (possibly encrypted) keying
material is sent to the other party, an ECDH
is performed based on exactly
which act is being executed with the result mixed into the current sent of
encryption keys (ck
the chainin gkey and k
the encryption key), and finally
an AEAD
payload with a zero length cipher text is sent. As this payload is
of length zero, only a MAC
is sent across. The mixing of ECDH
outputs into
a hash digest forms an incremental TripleDH handshake.
Using the language of the Noise Protocol, e
and s
(both public keys)
indicate possibly encrypted keying material, and es, ee, se
each indicate an
ECDH
operation between two keys. The handshake is laid out as follows:
Noise_XK(s, rs):
<- s
...
-> e, es
<- e, ee
-> s, se
All of the handshake data sent across the wire including the keying material is
incrementally hashed into a session-wide "handshake digest", h
. Note that the
handshake state h
, is never transmitted during the handshake, instead digest
is used as the Authenticated Data within the zero-length AEAD messages.
By authenticating each message sent, we can ensure that a MiTM hasn't modified or replaced any of the data sent across as part of a handshake, as the MAC check would fail on the other side if so.
A successful check of the MAC
by the receiver indicates implicitly that all
authentication has been successful up to that point. If MAC
check ever fails
during the handshake process, then the connection is to be immediately
terminated.
Handshake Versioning
Each message sent during the initial handshake starts with a single leading byte which indicates the version used for the current handshake. A version of 0 indicates that no change is necessary, while a non-zero version indicate the client has deviated from the protocol originally specified within this document. Clients MUST reject handshake attempts initiated with an unknown version.
Noise Protocol Instantiation
Concrete instantiations of the Noise Protocol require the definition of
three abstract cryptographic objects: the hash function, the elliptic curve,
and finally the AEAD
cipher scheme. Within our instantiation SHA-256
is
chosen as the hash function, secp256k1
as the elliptic curve, and finally
ChaChaPoly-1305
as the AEAD
construction. The composition of ChaCha20
and Poly1305
used MUST conform to RFC 7539
3. With this laid out, the
official Noise protocol name for our variant is:
Noise_XK_secp256k1_ChaChaPoly_SHA256
. The ascii string representation of
this value is hashed into a digest used to initialize the starting handshake
state. If the protocol names of two endpoints differs, then the handshake
process fails immediately.
Authenticated Key Exchange Handshake Specification
The handshake proceeds in three acts, taking 1.5 round trips. Each handshake is a fixed sized payload without any header or additional meta-data attached. The exact size of each Act is as follows:
- Act One:
50 bytes
- Act Two:
50 bytes
- Act Three:
66 bytes
Handshake State
Throughout the handshake process, each side maintains these three variables:
-
ck
: The chaining key. This value is the accumulated hash of all previous ECDH outputs. At the end of the handshake,ck
is used to derive the encryption keys for lightning messages. -
h
: The handshake hash. This value is the accumulated hash of all handshake data that has been sent and received so far during the handshake process. -
temp_k
: An intermediate key key used to encrypt/decrypt the zero-length AEAD payloads at the end of each handshake message. -
n
: A counter-based nonce which is to be used withtemp_k
to encrypt each message with a new nonce. -
e
: A party's ephemeral public key. For each session a node MUST generate a new ephemeral key with strong cryptographic randomness. -
s
: A party's static public key.
The following functions will also be referenced:
-
ECDH(rk, k)
: Performs an Elliptic-Curve Diffie-Hellman operation usingrk
which is asecp256k1
public key andk
which is a valid private key within the finite field as defined by the curve paramters.- The returned value is the raw big-endian byte serialization of
x-coordinate
(using affine coordinates) of the generated point.
- The returned value is the raw big-endian byte serialization of
-
HKDF
: a function is defined in 3, evaluated with a zero-lengthinfo
field.- All invocations of the
HKDF
implicitly return64-bytes
of cryptographic randomness using the extract-and-expand component of the `HKDF.
- All invocations of the
-
encryptWithAD(k, n, ad, plaintext)
: outputsencrypt(k, n++, ad, plaintext)
- where
encrypt
is an evaluation ofChaCha20-Poly1305
with the passed arguments.
- where
-
decryptWithAD(k, n, ad, ciphertext)
: outputsdecrypt(k, n++, ad, ciphertext)
- where
decrypt
is an evaluation ofChaCha20-Poly1305
with the passed arguments.
- where
-
generateKey()
- where generateKey generates and returns a fresh
secp256k1
keypair - the object returned by
generateKey
has two attributes:.pub
: which returns an abstract object representing the public key.priv
: which represents the private key used to generate the public key
- the object also has a single method:
.serializeCompressed()
- where generateKey generates and returns a fresh
-
a || b
denotes the concatenation of two byte stringsa
andb
Handshake State Initialization
Before the start of the first act, both sides initialize their per-sessions state as follows:
-
h = SHA-256(protocolName)
- where
protocolName = "Noise_XK_secp256k1_ChaChaPoly_SHA256"
encoded as an ascii string.
- where
-
ck = h
-
temp_k = empty
- where
empty
is a byte string of length 32 fully zeroed out.
- where
-
n = 0
-
h = SHA-256(h || prologue)
- where
prologue
is the ascii string:lightning
.
- where
As a concluding step, both sides mix the responder's public key into the handshake digest:
-
The initiating node mixes in the responding node's static public key serialized in Bitcoin's DER compressed format:
h = SHA-256(h || rs.pub.serializeCompressed())
-
The responding node mixes in their local static public key serialized in Bitcoin's DER compressed format:
h = SHA-256(h || ls.pub.serializeCompressed())
Handshake Exchange
Act One
-> e, es
Act One is sent from initiator to responder. During Act One
, the initiator
attempts to satisfy an implicit challenge by the responder. To complete this
challenge, the initiator must know the static public key of the responder.
The handshake message is exactly 50 bytes
: 1 byte
for the handshake
version, 33 bytes
for the compressed ephemeral public key of the initiator,
and 16 bytes
for the poly1305
tag.
Sender Actions:
-
e = generateKey()
-
h = SHA-256(h || e.pub.serializeCompressed())
- The newly generated ephemeral key is accumulated into our running handshake digest.
-
s = ECDH(rs, e.priv)
- The initiator performs a
ECDH
between its newly generated ephemeral key with the remote node's static public key.
- The initiator performs a
-
ck, temp_k = HKDF(ck, s)
- This phase generates a new temporary encryption key (
temp_k
) which is used to generate the authenticating MAC. - The nonce
n
should be reset to zero:n = 0
.
- This phase generates a new temporary encryption key (
-
c = encryptWithAD(temp_k, n, h, zero)
- where
zero
is a zero-length plaintext
- where
-
h = SHA-256(h || c)
- Finally, the generated ciphertext is accumulated into the authenticating handshake digest.
-
Send
m = 0 || e || c
to the responder over the network buffer.
Receiver Actions:
-
Read exactly
50-bytes
from the network buffer. -
Parse out the read message (
m
) intov = m[0]
,e = m[1:34]
andc = m[43:]
- where
m[0]
is the first byte ofm
,m[1:33]
are the next33
bytes ofm
andm[34:]
is the last 16 bytes ofm
- The raw bytes of the remote party's ephemeral public key (
e
) are to be deserialized into a point on the curve using affine coordinates as encoded by the key's serialized composed format.
- where
-
If
v
is an unrecognized handshake version, then the responder MUST abort the connection attempt. -
h = SHA-256(h || e.pub.serializeCompressed())
- Accumulate the initiator's ephemeral key into the authenticating handshake digest.
-
s = ECDH(e, s.priv)
- The responder performs an
ECDH
between its static public key and the initiator's ephemeral public key.
- The responder performs an
-
ck, temp_k = HKDF(ck, s)
- This phase generates a new temporary encryption key (
temp_k
) which will be used to shortly check the authenticating MAC. - The nonce
n
should be reset to zero:n = 0
.
- This phase generates a new temporary encryption key (
-
p = decryptWithAD(temp_k, n, h, c)
- If the MAC check in this operation fails, then the initiator does not know our static public key. If so, then the responder MUST terminate the connection without any further messages.
-
h = SHA-256(h || c)
- Mix the received ciphertext into the handshake digest. This step serves to ensure the payload wasn't modified by a MiTM.
Act Two
<- e, ee
Act Two
is sent from the responder to the initiator. Act Two
will only
take place if Act One
was successful. Act One
was successful if the
responder was able to properly decrypt and check the MAC
of the tag sent at
the end of Act One
.
The handshake is exactly 50 bytes:
1 byte
for the handshake version, 33 bytes
for the compressed ephemeral public key of the initiator, and 16 bytes
for the poly1305
tag.
Sender Actions:
-
e = generateKey()
-
h = SHA-256(h || e.pub.serializeCompressed())
- The newly generated ephemeral key is accumulated into our running handshake digest.
-
s = ECDH(re, e.priv)
- where
re
is the ephemeral key of the initiator which was received duringActOne
.
- where
-
ck, temp_k = HKDF(ck, s)
- This phase generates a new temporary encryption key (
temp_k
) which is used to generate the authenticating MAC. - The nonce
n
should be reset to zero:n = 0
.
- This phase generates a new temporary encryption key (
-
c = encryptWithAD(temp_k, n, h, zero)
- where
zero
is a zero-length plaintext
- where
-
h = SHA-256(h || c)
- Finally, the generated ciphertext is accumulated into the authenticating handshake digest.
-
Send
m = 0 || e || c
to the initiator over the network buffer.
Receiver Actions:
-
Read exactly
50-bytes
from the network buffer. -
Parse out the read message (
m
) intov = m[0]
, e = m[1:34]and
c = m[43:]`- where
m[0]
is the first byte ofm
,m[1:33]
are the next33
bytes ofm
andm[34:]
is the last 16 bytes ofm
- where
-
If
v
is an unrecognized handshake version, then the responder MUST abort the connection attempt. -
h = SHA-256(h || e.pub.serializeCompressed())
-
s = ECDH(re, e.priv)
- where
re
is the responder's ephemeral public key. - The raw bytes of the remote party's ephemeral public key (
e
) are to be deserialized into a point on the curve using affine coordinates as encoded by the key's serialized composed format.
- where
-
ck, temp_k = HKDF(ck, s)
- This phase generates a new temporary encryption key (
temp_k
) which is used to generate the authenticating MAC. - The nonce
n
should be reset to zero:n = 0
.
- This phase generates a new temporary encryption key (
-
p = decryptWithAD(temp_k, n, h, c)
- If the MAC check in this operation fails, then the initiator MUST terminate the connection without any further messages.
- The nonce
n
should be reset to zero:n = 0
.
-
h = SHA-256(h || c)
- Mix the received ciphertext into the handshake digest. This step serves to ensure the payload wasn't modified by a MiTM.
Act Three
-> s, se
Act Three
is the final phase in the authenticated key agreement described in
this section. This act is sent from the initiator to the responder as a final
concluding step. Act Three
is only executed iff
Act Two
was successful.
During Act Three
, the initiator transports its static public key to the
responder encrypted with strong forward secrecy using the accumulated HKDF
derived secret key at this point of the handshake.
The handshake is exactly 66 bytes
: 1 byte
for the handshake version, 33 bytes
for the ephemeral public key encrypted with the ChaCha20
stream
cipher, 16 bytes
for the encrypted public key's tag generated via the AEAD
construction, and 16 bytes
for a final authenticating tag.
Sender Actions:
-
c = encryptWithAD(temp_k, n, h, s.pub.serializeCompressed())
- where
s
is the static public key of the initiator.
- where
-
h = SHA-256(h || c)
-
s = ECDH(re, s.priv)
- where
re
is the ephemeral public key of the responder.
- where
-
ck, temp_k = HKDF(ck, s)
- Mix the final intermediate shared secret into the running chaining key.
- The nonce
n
should be reset to zero:n = 0
.
-
t = encryptWithAD(temp_k, n, h, zero)
- where
zero
is a zero-length plaintext
- where
-
sk, rk = HKDF(ck, zero)
-
where
zero
is a zero-length plaintext,sk
is the key to be used by the initiator to encrypt messages to the responder,and
rk
is the key to be used by the initiator to decrypt messages sent by the responder. -
This step generates the final encryption keys to be used for sending and receiving messages for the duration of the session.
-
-
Send
m = 0 || c || t
over the network buffer.
Receiver Actions:
-
Read exactly
66-bytes
from the network buffer. -
Parse out the read message (
m
) intov = m[0]
,c = m[1:50]
andt = m[50:]
-
If
v
is an unrecognized handshake version, then the responder MUST abort the connection attempt. -
rs = decryptWithAD(temp_k, n, h, c)
- At this point, the responder has recovered the static public key of the initiator.
-
h = SHA-256(h || rs.pub.serializeCompressed())
-
s = ECDH(rs, e.priv)
- where
e
is the responder's original ephemeral key
- where
-
ck, temp_k = HKDF(ck, s)
- The underscore denots that the final
32-bytes
generated by theHKDF
invocation are discarded. - The nonce
n
should be reset to zero:n = 0
.
- The underscore denots that the final
-
p = decryptWithAD(temp_k, n, h, t)
- If the MAC check in this operation fails, then the responder MUST terminate the connection without any further messages.
-
rk, sk = HKDF(ck, zero)
-
where
zero
is a zero-length plaintext,rk
is the key to be used by the responder to decrypt the messages sent by the responder,and
sk
is the key to be used by the initiator to encrypt messages to the responder, -
This step generates the final encryption keys to be used for sending and receiving messages for the duration of the session.
-
Lightning Message Specification
At the conclusion of Act Three
both sides have derived the encryption keys
which will be used to encrypt/decrypt messages for the remainder of the
session.
The actual lightning protocol messages are encapsulated within AEAD
ciphertexts. Each message is prefixed with
another AEAD
ciphertext which encodes the total length of the following lightning
message (not counting its MAC).
The maximum size of any lightning message MUST NOT exceed 65535
bytes. A
maximum size of 65535
simplifies testing, makes memory management
easier and helps mitigate memory exhaustion attacks.
In order to make make traffic analysis more difficult, the length prefix for
all encrypted lightning messages is also encrypted. Additionally we add a
16-byte
Poly-1305
tag to the encrypted length prefix in order to ensure
that the packet length hasn't been modified with in-flight, and also to avoid
creating a decryption oracle.
The structure of packets on the wire resembles the following:
+-------------------------------
|2-byte encrypted message length|
+-------------------------------
| 16-byte MAC of the encrypted |
| message length |
+-------------------------------
| |
| |
| encrypted lightning |
| message |
| |
+-------------------------------
| 16-byte MAC of the |
| lightning message |
+-------------------------------
The prefixed message length is encoded as a 2-byte
big-endian integer,
for a total maximum packet length of 2 + 16 + 65535 + 16
= 65569
bytes.
Encrypting Messages
In order to encrypt a lightning message (m
), given a sending key (sk
), and a nonce
(n
), the following is done:
-
let
l = len(m)
, wherelen
obtains the length in bytes of the lightning message. -
Serialize
l
into2-bytes
encoded as a big-endian integer. -
Encrypt
l
usingChaChaPoly-1305
,n
, andsk
to obtainlc
(18-bytes
)- The nonce for
sk
MUST be incremented after this step. - A zero-length byte slice is to be passed as the AD (associated data).
- The nonce for
-
Finally encrypt the message itself (
m
) using the same procedure used to encrypt the length prefix. Let encrypted ciphertext be known asc
.- The nonce for
sk
MUST be incremented after this step.
- The nonce for
-
Send
lc || c
over the network buffer.
Decrypting Messages
In order to decrypt the next message in the network stream, the following is done:
-
Read exactly
18-bytes
from the network buffer. -
Let the encrypted length prefix be known as
lc
-
Decrypt
lc
usingChaCha20-Poy1305
,n
, andrk
to obtain size of the encrypted packetl
.- A zero-length byte slice is to be passed as the AD (associated data).
- The nonce for
rk
MUST be incremented after this step.
-
Read exactly
l+16
bytes from the network buffer, let the bytes be known asc
. -
Decrypt
c
usingChaCha20-Poly1305
,n
, andrk
to obtain decrypted plaintext packetp
.- The nonce for
rk
MUST be incremented after this step.
- The nonce for
Lightning Message Key Rotation
Changing keys regularly and forgetting the previous key is useful for preventing decryption of old messages in the case of later key leakage (ie. backwards secrecy).
Key rotation is performed for each key (sk
and rk
) _individually _. A key
is to be rotated after a party sends of decrypts 1000
messages with it.
This can be properly accounted for by rotating the key once the nonce dedicated
to it exceeds 1000
.
Key rotation for a key k
is performed according to the following:
- Let
ck
be the chaining key obtained at the end ofAct Three
. ck', k' = HKDF(ck, k)
- Reset the nonce for the key to
n = 0
. k = k'
ck = ck'
Security Considerations
It is strongly recommended that existing, commonly-used, validated libraries be used for encryption and decryption, to avoid the many implementation pitfalls possible.
Acknowledgements
TODO(roasbeef); fin
References
Authors
FIXME
This work is licensed under a Creative Commons Attribution 4.0 International License.