BOLT 8: Encrypted and Authenticated Transport
All communications between Lightning nodes is encrypted in order to provide confidentiality for all transcripts between nodes and is authenticated in order to avoid malicious interference. Each node has a known long-term identifier that is a public key on Bitcoin's `secp256k1` curve. This long-term public key is used within the protocol to establish an encrypted and authenticated connection with p
No reviewsSpecification
BOLT #8: Encrypted and Authenticated Transport
All communications between Lightning nodes is encrypted in order to
provide confidentiality for all transcripts between nodes and is authenticated in order to
avoid malicious interference. Each node has a known long-term identifier that
is a public key on Bitcoin's secp256k1 curve. This long-term public key is
used within the protocol to establish an encrypted and authenticated connection
with peers, and also to authenticate any information advertised on behalf
of a node.
Table of Contents
- Cryptographic Messaging Overview
- Authenticated Key Exchange Handshake Specification
- Lightning Message Specification
- Lightning Message Key Rotation
- Security Considerations
- Appendix A: Transport Test Vectors
- Acknowledgments
- References
- Authors
Cryptographic Messaging Overview
Prior to sending any Lightning messages, nodes MUST first initiate the cryptographic session state that 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:
- Before any actual data transfer, both nodes participate in an authenticated key agreement handshake, which is based on the Noise Protocol Framework<sup>2</sup>.
- If the initial handshake is successful, then nodes enter the Lightning message exchange phase. In the Lightning message exchange phase, all messages are Authenticated Encryption with Associated Data (AEAD) ciphertexts.
Authenticated Key Agreement Handshake
The handshake chosen for the authenticated key exchange is Noise_XK. As a
pre-message, the initiator must know the identity public key of
the responder. This provides a degree of identity hiding for the
responder, as its static public key is never transmitted during the handshake. Instead,
authentication is achieved implicitly via a series of Elliptic-Curve
Diffie-Hellman (ECDH) operations followed by a MAC check.
The authenticated key agreement (Noise_XK) is performed in three distinct
steps (acts). During each act of the handshake the following occurs: 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 set of
encryption keys (ck the chaining key and k the encryption key); and
an AEAD payload with a zero-length cipher text is sent. As this payload has no
length, 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 with e being
the ephemeral key and s being the static key which in our case is usually the nodeid)
indicate possibly encrypted keying material, and es, ee, and 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 Associated Data within the zero-length AEAD messages.
Authenticating each message sent ensures that a man-in-the-middle (MITM) hasn't modified or replaced any of the data sent 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 a 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 that 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 the AEAD cipher scheme. For Lightning, SHA-256 is
chosen as the hash function, secp256k1 as the elliptic curve, and
ChaChaPoly-1305 as the AEAD construction.
The composition of ChaCha20 and Poly1305 that are used MUST conform to
RFC 8439<sup>1</sup>.
The official protocol name for the Lightning variant of Noise 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 differ, 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 variables:
-
ck: the chaining key. This value is the accumulated hash of all previous ECDH outputs. At the end of the handshake,ckis 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_k1,temp_k2,temp_k3: the intermediate keys. These are used to encrypt and decrypt the zero-length AEAD payloads at the end of each handshake message. -
e: a party's ephemeral keypair. For each session, a node MUST generate a new ephemeral key with strong cryptographic randomness. -
s: a party's static keypair (lsfor local,rsfor remote)
The following functions will also be referenced:
-
ECDH(k, rk): performs an Elliptic-Curve Diffie-Hellman operation usingk, which is a validsecp256k1private key, andrk, which is a valid public key- The returned value is the SHA256 of the compressed format of the generated point.
-
HKDF(salt,ikm): a function defined inRFC 5869<sup>3</sup>, evaluated with a zero-lengthinfofield- All invocations of
HKDFimplicitly return 64 bytes of cryptographic randomness using the extract-and-expand component of theHKDF.
- All invocations of
-
encryptWithAD(k, n, ad, plaintext): outputsencrypt(k, n, ad, plaintext)- Where
encryptis an evaluation ofChaCha20-Poly1305(IETF variant) with the passed arguments, with noncenencoded as 32 zero bits, followed by a little-endian 64-bit value. Note: this follows the Noise Protocol convention, rather than our normal endian.
- Where
-
decryptWithAD(k, n, ad, ciphertext): outputsdecrypt(k, n, ad, ciphertext)- Where
decryptis an evaluation ofChaCha20-Poly1305(IETF variant) with the passed arguments, with noncenencoded as 32 zero bits, followed by a little-endian 64-bit value.
- Where
-
generateKey(): generates and returns a freshsecp256k1keypair- Where the object returned by
generateKeyhas two attributes:.pub, which returns an abstract object representing the public key.priv, which represents the private key used to generate the public key
- Where the object also has a single method:
.serializeCompressed()
- Where the object returned by
-
a || bdenotes the concatenation of two byte stringsaandb
Handshake State Initialization
Before the start of Act One, 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 -
h = SHA-256(h || prologue)- where
prologueis 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 compressed format:
h = SHA-256(h || rs.pub.serializeCompressed())
-
The responding node mixes in their local static public key serialized in Bitcoin's 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 the running handshake digest.
es = ECDH(e.priv, rs)- The initiator performs an ECDH between its newly generated ephemeral key and the remote node's static public key.
ck, temp_k1 = HKDF(ck, es)- A new temporary encryption key is generated, which is used to generate the authenticating MAC.
c = encryptWithAD(temp_k1, 0, h, zero)- where
zerois 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.pub.serializeCompressed() || cto the responder over the network buffer.
Receiver Actions:
- Read exactly 50 bytes from the network buffer.
- Parse the read message (
m) intov,re, andc:- where
vis the first byte ofm,reis the next 33 bytes ofm, andcis the last 16 bytes ofm - The raw bytes of the remote party's ephemeral public key (
re) are to be deserialized into a point on the curve using affine coordinates as encoded by the key's serialized composed format.
- where
- If
vis an unrecognized handshake version, then the responder MUST abort the connection attempt. h = SHA-256(h || re.serializeCompressed())- The responder accumulates the initiator's ephemeral key into the authenticating handshake digest.
es = ECDH(s.priv, re)- The responder performs an ECDH between its static private key and the initiator's ephemeral public key.
ck, temp_k1 = HKDF(ck, es)- A new temporary encryption key is generated, which will shortly be used to check the authenticating MAC.
p = decryptWithAD(temp_k1, 0, h, c)- If the MAC check in this operation fails, then the initiator does not know the responder's static public key. If this is the case, then the responder MUST terminate the connection without any further messages.
h = SHA-256(h || c)- The received ciphertext is mixed 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 responder, 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 the running handshake digest.
ee = ECDH(e.priv, re)- where
reis the ephemeral key of the initiator, which was received during Act One
- where
ck, temp_k2 = HKDF(ck, ee)- A new temporary encryption key is generated, which is used to generate the authenticating MAC.
c = encryptWithAD(temp_k2, 0, h, zero)- where
zerois 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.pub.serializeCompressed() || cto the initiator over the network buffer.
Receiver Actions:
- Read exactly 50 bytes from the network buffer.
- Parse the read message (
m) intov,re, andc:- where
vis the first byte ofm,reis the next 33 bytes ofm, andcis the last 16 bytes ofm.
- where
- If
vis an unrecognized handshake version, then the responder MUST abort the connection attempt. h = SHA-256(h || re.serializeCompressed())ee = ECDH(e.priv, re)- where
reis the responder's ephemeral public key - The raw bytes of the remote party's ephemeral public key (
re) 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_k2 = HKDF(ck, ee)- A new temporary encryption key is generated, which is used to generate the authenticating MAC.
p = decryptWithAD(temp_k2, 0, h, c)- If the MAC check in this operation fails, then the initiator MUST terminate the connection without any further messages.
h = SHA-256(h || c)- The received ciphertext is mixed 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
[Content truncated — view full spec at source]
Discussion (0 threads)
No discussion yet. Be the first to comment.