mirror of
https://github.com/lnbook/lnbook
synced 2024-11-18 21:28:03 +00:00
Merge pull request #701 from Roasbeef/brontide-chapter
ch-brontide: add initial draft of the brontide chapter
This commit is contained in:
commit
e5b290ad13
603
brontide.asciidoc
Normal file
603
brontide.asciidoc
Normal file
@ -0,0 +1,603 @@
|
||||
= Brontide: Lightning's Encrypted Message Transport
|
||||
|
||||
== Intro
|
||||
|
||||
Unlike the vanilla Bitcoin P2P network, every node in the Lightning Network is
|
||||
identified by a unique public key which serves as it identity. By default, this
|
||||
public key is used to end-to-end encrypt _all_ communication within the
|
||||
network. Encryption by default at the lowest level of the protocol ensures that
|
||||
all messages are authenticated, are immune to man-in-the-middle attacks,
|
||||
snooping by 3rd parties, and ensures privacy at the fundamental transport
|
||||
level. In this chapter, we'll learn about the encryption protocol used by the
|
||||
Lightning network in detail. Upon completion of this chapter, the reader will
|
||||
be familiar with the state of the art in encrypted messaging protocols, as well
|
||||
as the various properties such a protocol provides to the network. It's worth
|
||||
mentioning that the core of the encrypted message transport is _agonstic_ to
|
||||
its usage within the context of the Lightning Network. As a result, the
|
||||
custom encrypted message transport Lightning uses, commonly referred to as
|
||||
"Brontide" (more on that later) can be dropped into any context that requires
|
||||
encrypted communication between two parties.
|
||||
|
||||
== The Channel Graph as Decentralized Public Key Infrastructure
|
||||
|
||||
As we learned in the chapter on multi-hop forwarding, very node has a long-term
|
||||
identity that is used as the identifier for a vertex during path finding and
|
||||
also used in the asymmetric cryptographic operations related to the creation of
|
||||
onion encrypted routing packets. This public key, which serves as a node's
|
||||
long-term identity is included in the DNS bootstrapping response, as well as
|
||||
embedded within the Channel Graph. As a result, before a node attempts to
|
||||
connect out to another node on the P2P network, it already knows the public key
|
||||
of the node it wishes to connect to.
|
||||
|
||||
Additionally, if the node being connected to already h a series of public
|
||||
channels within the graph, then the connecting node is able to further verify
|
||||
the sanctity of the identity of the node. As the entire channel graph is fully
|
||||
authenticated, one can view it as a sort of decentralized public key
|
||||
infrastructure: in order to register a key, a public channel in the Bitcoin
|
||||
blockchain must be opened, once a node no longer has any public channels, then
|
||||
they've effectively been removed from the PKI.
|
||||
|
||||
As Lightning is a decentralized network, it's imperative that no one central
|
||||
party is designated the power to provision a public key identity within the
|
||||
network. In place of a central party, the Lightning Network uses the Bitcoin
|
||||
blockchain as a sybil mitigation mechanism, as gaining an identity on the
|
||||
network has a tangible cost: the fee needed to create a channel in the
|
||||
blockchain, as well as the opportunity cost of the capital allocated to their
|
||||
channels. In the process of essentially rolling a domain specific PKI, the
|
||||
Lightning network is able to significantly simply its encrypted transport
|
||||
protocol as it doesn't need to deal with all the complexities that come along
|
||||
with TLS, the Transport Layer Security protocol.
|
||||
|
||||
== Why Not TLS?
|
||||
|
||||
Readers familiar with the TLS system may be wondering at this point: why wasn't
|
||||
TLS used in spite of the drawbacks of the existing PKI system? It is indeed a
|
||||
fact that "self signed certificates" can be used to effectively sidestep the
|
||||
existing global PKI system by simply asserting to the identity of a given
|
||||
public key amongst a set of peers. However, even with the existing PKI system
|
||||
out of the way, TLS has several drawbacks that prompted the creators of the LN
|
||||
to instead opt for a more compact custom encryption protocol.
|
||||
|
||||
To start with, TLS is a protocol that has been around for several decades and
|
||||
as a result has evolved over time as new advances have been made in the space
|
||||
of transport encryption. However, overtime this evolution has caused the
|
||||
protocol to balloon in size and complexity. Over the past few decades several
|
||||
vulnerabilities in TLS has been discovered, and patched with each evolution
|
||||
further increasing the complexity of the protocol. As a result of the age of
|
||||
the protocol several versions and iterations exist, meaning a client needs to
|
||||
understand many of the prior iterations of the protocol in order to communicate
|
||||
with a large portion of the public internet further increasing implementation
|
||||
complexity.
|
||||
|
||||
In the past several memory safety vulnerabilities have been discovered in
|
||||
widely used implementations of SSL/TLS. Packaging such a protocol within every
|
||||
Lightning node would serve to increase the attack surface of nodes exposed to
|
||||
to the public peer to peer network. In order to increase the security of the
|
||||
network as a whole, and minimize exploitable attack surface, the creators of
|
||||
the LN instead opted to adopt the Noise Protocol Framework. Noise as a protocol
|
||||
internalizes several of the security and privacy lessons learned over time due
|
||||
to continual scrutiny of the TLS protocol over decades. In a way, the existence
|
||||
of Noise allows the community to effective "start over", with a more compact,
|
||||
simplified protocol that retains all the added benefits of TLS.
|
||||
|
||||
== The Noise Protocol Framework
|
||||
|
||||
The Noise Protocol Framework is a modern, extensible, and flexible message
|
||||
encryption protocol designed by the creators of the Signal protocol. The Signal
|
||||
protocol is one of the most widely used message encryption protocols in the
|
||||
world. It's used by both Signal and Whatsapp, which cumulatively are used by
|
||||
over a billion people around the world. The Noise framework is the result of
|
||||
decades of evolution both within academia as well as industry of message
|
||||
encryption protocols. Lightning uses the Noise protocol framework to implement
|
||||
a _message oriented_ encryption protocol used by all nodes to communicate with
|
||||
each other.
|
||||
|
||||
A communication session using Noise has two distinct phases: the handshake
|
||||
phase, and the messaging phase. Before two parties can communicate with each
|
||||
other, they first need to arrive at a shared secret known only to them which
|
||||
will be used to encrypt and authenticate messages sent to each other. A flavor
|
||||
of an authenticated key agreement is used to arrive at a final shared key
|
||||
between the tow parties. In the context of the Noise protocol, this
|
||||
authenticated key agreement is referred to as a "handshake". Once that
|
||||
handshake has been completed, both nodes can now being to send each other
|
||||
encrypted messages. Each time peers need to connect, or reconnect to each
|
||||
other, a fresh iteration of the handshake protocol is executed ensuring that
|
||||
forward secrecy (leaking the key of a prior transcript doesn't compromise any
|
||||
future transcripts) is achieved.
|
||||
|
||||
As the Noise protocol allows a protocol designer to drop choose from several
|
||||
cryptographic primitives such as symmetric encryption and public key
|
||||
cryptography, its customary that each flavor of the Noise protocol is referred
|
||||
to by a unique name. In the spirit of "Noise", each flavor of the protocol
|
||||
selects a name derived from some sort of "noise". In the context of the
|
||||
Lightning Network, the flavor of Noise use will be referred to from here on as
|
||||
"Brontide". A brontide is a low billowing noise, similar to what one would hear
|
||||
during a thunderstorm when very far away.
|
||||
|
||||
=== Noise Protocol Handshakes
|
||||
|
||||
The Noise protocol is extremely flexible in that it advertises several
|
||||
handshakes, each with different security and privacy properties for a would be
|
||||
protocol implementer to select from. A deep exploration of each of the
|
||||
handshakes, and their various trade-offs is out of the scope of this chapter.
|
||||
With that said, the Lighting Network uses a specific handshake referred to as
|
||||
`Noise_XK`. The unique property provided by this handshake is "identity
|
||||
hiding": in order for a node to initiate a connection with another node, it
|
||||
must first know it's public key. Mechanically, this means that the public key
|
||||
of the responder is actually never transmitted during the context of the
|
||||
handshake. Instead, a clever series of Elliptic-Curve Diffie-Hellman (ECDH) and
|
||||
Message Authentication Code (MAC) checks are used to authenticate the
|
||||
responder.
|
||||
|
||||
=== Handshake Notation & Protocol Flow
|
||||
|
||||
Each handshakes typically consist of several steps. At each step some
|
||||
(possibly) encrypted material is sent to the opposite party, an ECDH (or
|
||||
several) are performed, with the result of the handshake being "mixed" into a
|
||||
protocol "transcript". This transcript serves to authenticate each step of the
|
||||
protocol and helps thwart a flavor of main-man-in-the-middle attacks. At the
|
||||
end of the handshake, two keys `ck` and `k` are produced which are used to
|
||||
encrypt messages (`k`) as well as rotate keys (`ck`) throughout the lifetime of
|
||||
the session.
|
||||
|
||||
In the context of a handshake, `s` is usually a long-term static public key.
|
||||
Within Brontide, the public key crypto system used is an elliptic curve one,
|
||||
instantiated with the `secp256k1` curve which is used elsewhere in Bitcoin.
|
||||
Several ephemeral keys are generated throughout the handshake. We use `e` to
|
||||
refer to a new ephemeral key. ECDH operations between two keys are notated as
|
||||
the concatenation of two keys. As an example, `ee` represents an ECDH operation
|
||||
between two ephemeral keys.
|
||||
|
||||
== Brontide: Lightning's P2P Encryption
|
||||
|
||||
=== High-Level Overview
|
||||
|
||||
Using the notation laid out earlier, we can succinctly describe the `Noise_XK`
|
||||
as follows:
|
||||
```
|
||||
Noise_XK(s, rs):
|
||||
<- rs
|
||||
...
|
||||
-> e, e(rs)
|
||||
<- e, ee
|
||||
-> s, se
|
||||
```
|
||||
|
||||
The protocol begins with the "pre-transmission" of the responder's static key
|
||||
(`rs)` to the initiator. Before executing the handshake, the initiator is to
|
||||
generate its own static key (`s`). During each step of the handshake, all
|
||||
material sent across the wire, as well as the keys sent/used are incrementally
|
||||
hashed into a "handshake digest", `h`. This digest is never sent across the
|
||||
wire during the handshake, and is instead used as the "Associated Data" when an
|
||||
AEAD (authenticated encryption w/ associated data) is sent across the wire.
|
||||
Associated data allows an encryption protocol to authenticate additional
|
||||
information along side a cipher text packet. In other domains, the AD may be a
|
||||
domain name, or plaintext portion of the packet.
|
||||
|
||||
The existence of `h` ensures that if a portion of a transmitted handshake
|
||||
message is replaced, then the other side will notice. At each step, a MAC
|
||||
digest is checked. If the MAC check succeeds, then the receiving party knows
|
||||
that the handshake has been successful up until that point. Otherwise if a MAC
|
||||
check ever fails, then the handshake process has failed, and the connection
|
||||
should be terminated.
|
||||
|
||||
Brontide also adds a new piece of data to each handshake message: a protocol
|
||||
version. The initial protocol version is `0`. At the time of writing, no new
|
||||
protocol versions has been created. As a result, if a peer receives a version
|
||||
other than `0`, then they should reject the handshake initiation attempt.
|
||||
|
||||
As far as cryptographic primitives, `SHA-256` is used as the hash function of
|
||||
choice, `secp256k1` as the elliptic curve, and `ChaChaPoly-130` as the AEAD
|
||||
(symmetric encryption) construction.
|
||||
|
||||
Each variant of the Noise protocol has a unique ASCII string used to uniquely
|
||||
refer to it. In order to ensure that two parties are using the same protocol
|
||||
variant, the ASCII string is hashed into a digest, which is used to initialize
|
||||
the starting handshake state. In the context of Brontide, the ASCII string
|
||||
describing the protocol is: `Noise_XK_secp256k1_ChaChaPoly_SHA256`.
|
||||
|
||||
=== Brontide: A Handshake in Three Acts
|
||||
|
||||
The handshake portion of Brontide can be see prated into three distinct "acts".
|
||||
The entire handshake takes 1.5 round trips between the initiator and responder.
|
||||
At each act, a single message is sent between both parties. The handshake
|
||||
message is a _fixed_ sized payload prefixed by the protocol version.
|
||||
|
||||
The Noise protocol uses an object oriented inspired notation to describe the
|
||||
protocol at each step. During set up of the handshake state, each side will
|
||||
initialize the following "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_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** (`ls` for local, `rs` for remote)
|
||||
|
||||
Given this handshake+messaging session state, we'll then define a series of
|
||||
functions that will operate on the handshake and messaging state. When
|
||||
describing the handshake protocol, we'll use these variables in a manner
|
||||
similar to pseudo-code in order to reduce the verbosity of the explanation of
|
||||
each step in the protocol. We'll define the _functional_ primitives of the
|
||||
handshake as:
|
||||
|
||||
* `ECDH(k, rk)`: performs an Elliptic-Curve Diffie-Hellman operation using
|
||||
`k`, which is a valid `secp256k1` private key, and `rk`, 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 in `RFC 5869`<sup>[3](#reference-3)</sup>,
|
||||
evaluated with a zero-length `info` field
|
||||
* All invocations of `HKDF` implicitly return 64 bytes of
|
||||
cryptographic randomness using the extract-and-expand component of the
|
||||
`HKDF`.
|
||||
|
||||
* `encryptWithAD(k, n, ad, plaintext)`: outputs `encrypt(k, n, ad, plaintext)`
|
||||
* Where `encrypt` is an evaluation of `ChaCha20-Poly1305` (IETF variant)
|
||||
with the passed arguments, with nonce `n` encoded as 32 zero bits,
|
||||
followed by a *little-endian* 64-bit value. Note: this follows the Noise
|
||||
Protocol convention, rather than our normal endian.
|
||||
|
||||
* `decryptWithAD(k, n, ad, ciphertext)`: outputs `decrypt(k, n, ad, ciphertext)`
|
||||
* Where `decrypt` is an evaluation of `ChaCha20-Poly1305` (IETF variant)
|
||||
with the passed arguments, with nonce `n` encoded as 32 zero bits,
|
||||
followed by a *little-endian* 64-bit value.
|
||||
|
||||
* `generateKey()`: generates and returns a fresh `secp256k1` keypair
|
||||
* Where 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
|
||||
* Where the object also has a single method:
|
||||
* `.serializeCompressed()`
|
||||
|
||||
* `a || b` denotes the concatenation of two byte strings `a` and `b`
|
||||
|
||||
==== Handshake Session State Initialization
|
||||
|
||||
Before starting the handshake process, both sides need to initialize the
|
||||
starting state that they'll use to advance the handshake process. To start,
|
||||
both sides need to construct the initial handshake digest `h` which will be
|
||||
used as the initial handshake digest.
|
||||
|
||||
1. `h = SHA-256(protocolName)`
|
||||
* where `protocolName = "Noise_XK_secp256k1_ChaChaPoly_SHA256"` encoded as
|
||||
an ASCII string
|
||||
|
||||
2. `ck = h`
|
||||
|
||||
3. `h = SHA-256(h || prologue)`
|
||||
* where `prologue` is the ASCII string: `lightning`
|
||||
|
||||
In addition to the protocol name, we also add in an extra "prologue" that is
|
||||
used to further bind the protocol context to the Lightning network.
|
||||
|
||||
To conclude the initialization step, both sides mix the responder's public key
|
||||
into the handshake digest. As this digest is used as the associated data with a
|
||||
zero-length ciphertext (only the MAC) is sent, this ensures that the initiator
|
||||
does indeed know the public key of the responder.
|
||||
|
||||
* 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 Acts
|
||||
|
||||
After the initial handshake initialization, we can begin the actual execution
|
||||
of the handshake process. The Brontide handshake is compromised of a series of
|
||||
three messages sent between the initiator and responder, hence referred to as
|
||||
"acts". As each act is a single message sent between the parties, a handshake
|
||||
is completed in a total of 1.5 round trips (0.5 for each act).
|
||||
|
||||
The first act completes the initial portion of the incremental Triple Diffie
|
||||
Hellman key exchange (using a new ephemeral key generated by the initiator),
|
||||
and also ensures that the initiator actually knows the long-term public key of
|
||||
the responder. During the second act, the responder transmits the thermal key
|
||||
they wish to use for the session to the initiator, and one again incrementally
|
||||
mixes this new key into the Triple DH handshake. During the third and final
|
||||
act, the initiator transmits their long-term static public key to the
|
||||
responder, and executes the final DH operation to mix that into the final
|
||||
resulting shared secret.
|
||||
|
||||
===== 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:**
|
||||
|
||||
1. `e = generateKey()`
|
||||
2. `h = SHA-256(h || e.pub.serializeCompressed())`
|
||||
* The newly generated ephemeral key is accumulated into the running
|
||||
handshake digest.
|
||||
3. `es = ECDH(e.priv, rs)`
|
||||
* The initiator performs an ECDH between its newly generated ephemeral
|
||||
key and the remote node's static public key.
|
||||
4. `ck, temp_k1 = HKDF(ck, es)`
|
||||
* A new temporary encryption key is generated, which is
|
||||
used to generate the authenticating MAC.
|
||||
5. `c = encryptWithAD(temp_k1, 0, h, zero)`
|
||||
* where `zero` is a zero-length plaintext
|
||||
6. `h = SHA-256(h || c)`
|
||||
* Finally, the generated ciphertext is accumulated into the authenticating
|
||||
handshake digest.
|
||||
7. Send `m = 0 || e.pub.serializeCompressed() || c` to the responder over the network buffer.
|
||||
|
||||
**Receiver Actions:**
|
||||
|
||||
1. Read _exactly_ 50 bytes from the network buffer.
|
||||
2. Parse the read message (`m`) into `v`, `re`, and `c`:
|
||||
* where `v` is the _first_ byte of `m`, `re` is the next 33
|
||||
bytes of `m`, and `c` is the last 16 bytes of `m`
|
||||
* 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.
|
||||
3. If `v` is an unrecognized handshake version, then the responder MUST
|
||||
abort the connection attempt.
|
||||
4. `h = SHA-256(h || re.serializeCompressed())`
|
||||
* The responder accumulates the initiator's ephemeral key into the authenticating
|
||||
handshake digest.
|
||||
5. `es = ECDH(s.priv, re)`
|
||||
* The responder performs an ECDH between its static private key and the
|
||||
initiator's ephemeral public key.
|
||||
6. `ck, temp_k1 = HKDF(ck, es)`
|
||||
* A new temporary encryption key is generated, which will
|
||||
shortly be used to check the authenticating MAC.
|
||||
7. `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.
|
||||
8. `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:**
|
||||
|
||||
1. `e = generateKey()`
|
||||
2. `h = SHA-256(h || e.pub.serializeCompressed())`
|
||||
* The newly generated ephemeral key is accumulated into the running
|
||||
handshake digest.
|
||||
3. `ee = ECDH(e.priv, re)`
|
||||
* where `re` is the ephemeral key of the initiator, which was received
|
||||
during Act One
|
||||
4. `ck, temp_k2 = HKDF(ck, ee)`
|
||||
* A new temporary encryption key is generated, which is
|
||||
used to generate the authenticating MAC.
|
||||
5. `c = encryptWithAD(temp_k2, 0, h, zero)`
|
||||
* where `zero` is a zero-length plaintext
|
||||
6. `h = SHA-256(h || c)`
|
||||
* Finally, the generated ciphertext is accumulated into the authenticating
|
||||
handshake digest.
|
||||
7. Send `m = 0 || e.pub.serializeCompressed() || c` to the initiator over the network buffer.
|
||||
|
||||
**Receiver Actions:**
|
||||
|
||||
1. Read _exactly_ 50 bytes from the network buffer.
|
||||
2. Parse the read message (`m`) into `v`, `re`, and `c`:
|
||||
* where `v` is the _first_ byte of `m`, `re` is the next 33
|
||||
bytes of `m`, and `c` is the last 16 bytes of `m`.
|
||||
3. If `v` is an unrecognized handshake version, then the responder MUST
|
||||
abort the connection attempt.
|
||||
4. `h = SHA-256(h || re.serializeCompressed())`
|
||||
5. `ee = ECDH(e.priv, re)`
|
||||
* where `re` is 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.
|
||||
6. `ck, temp_k2 = HKDF(ck, ee)`
|
||||
* A new temporary encryption key is generated, which is
|
||||
used to generate the authenticating MAC.
|
||||
7. `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.
|
||||
8. `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 key agreement described in
|
||||
this section. This act is sent from the initiator to the responder as a
|
||||
concluding step. Act Three is executed _if and only if_ 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 static 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:**
|
||||
|
||||
1. `c = encryptWithAD(temp_k2, 1, h, s.pub.serializeCompressed())`
|
||||
* where `s` is the static public key of the initiator
|
||||
2. `h = SHA-256(h || c)`
|
||||
3. `se = ECDH(s.priv, re)`
|
||||
* where `re` is the ephemeral public key of the responder
|
||||
4. `ck, temp_k3 = HKDF(ck, se)`
|
||||
* The final intermediate shared secret is mixed into the running chaining key.
|
||||
5. `t = encryptWithAD(temp_k3, 0, h, zero)`
|
||||
* where `zero` is a zero-length plaintext
|
||||
6. `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
|
||||
* The final encryption keys, to be used for sending and
|
||||
receiving messages for the duration of the session, are generated.
|
||||
7. `rn = 0, sn = 0`
|
||||
* The sending and receiving nonces are initialized to 0.
|
||||
8. Send `m = 0 || c || t` over the network buffer.
|
||||
|
||||
**Receiver Actions:**
|
||||
|
||||
1. Read _exactly_ 66 bytes from the network buffer.
|
||||
2. Parse the read message (`m`) into `v`, `c`, and `t`:
|
||||
* where `v` is the _first_ byte of `m`, `c` is the next 49
|
||||
bytes of `m`, and `t` is the last 16 bytes of `m`
|
||||
3. If `v` is an unrecognized handshake version, then the responder MUST
|
||||
abort the connection attempt.
|
||||
4. `rs = decryptWithAD(temp_k2, 1, h, c)`
|
||||
* At this point, the responder has recovered the static public key of the
|
||||
initiator.
|
||||
5. `h = SHA-256(h || c)`
|
||||
6. `se = ECDH(e.priv, rs)`
|
||||
* where `e` is the responder's original ephemeral key
|
||||
7. `ck, temp_k3 = HKDF(ck, se)`
|
||||
8. `p = decryptWithAD(temp_k3, 0, h, t)`
|
||||
* If the MAC check in this operation fails, then the responder MUST
|
||||
terminate the connection without any further messages.
|
||||
9. `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 initiator,
|
||||
and `sk` is the key to be used by the responder to encrypt messages to
|
||||
the initiator
|
||||
* The final encryption keys, to be used for sending and
|
||||
receiving messages for the duration of the session, are generated.
|
||||
10. `rn = 0, sn = 0`
|
||||
* The sending and receiving nonces are initialized to 0.
|
||||
|
||||
==== Transport Message Encryption
|
||||
|
||||
At the conclusion of Act Three, both sides have derived the encryption keys, which
|
||||
will be used to encrypt and 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 including 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 traffic analysis more difficult, the length prefix for all
|
||||
encrypted Lightning messages is also encrypted. Additionally a 16-byte
|
||||
`Poly-1305` tag is added to the encrypted length prefix in order to ensure that
|
||||
the packet length hasn't been modified when 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 and Sending Messages
|
||||
|
||||
In order to encrypt and send a Lightning message (`m`) to the network stream,
|
||||
given a sending key (`sk`) and a nonce (`sn`), the following steps are
|
||||
completed:
|
||||
|
||||
1. Let `l = len(m)`.
|
||||
* where `len` obtains the length in bytes of the Lightning message
|
||||
2. Serialize `l` into 2 bytes encoded as a big-endian integer.
|
||||
3. Encrypt `l` (using `ChaChaPoly-1305`, `sn`, and `sk`), to obtain `lc`
|
||||
(18 bytes)
|
||||
* The nonce `sn` is encoded as a 96-bit little-endian number. As the
|
||||
decoded nonce is 64 bits, the 96-bit nonce is encoded as: 32 bits
|
||||
of leading 0s followed by a 64-bit value.
|
||||
* The nonce `sn` MUST be incremented after this step.
|
||||
* A zero-length byte slice is to be passed as the AD (associated data).
|
||||
4. Finally, encrypt the message itself (`m`) using the same procedure used to
|
||||
encrypt the length prefix. Let encrypted ciphertext be known as `c`.
|
||||
* The nonce `sn` MUST be incremented after this step.
|
||||
5. Send `lc || c` over the network buffer.
|
||||
|
||||
===== Receiving and Decrypting Messages
|
||||
|
||||
In order to decrypt the _next_ message in the network stream, the following
|
||||
steps are completed:
|
||||
|
||||
1. Read _exactly_ 18 bytes from the network buffer.
|
||||
2. Let the encrypted length prefix be known as `lc`.
|
||||
3. Decrypt `lc` (using `ChaCha20-Poly1305`, `rn`, and `rk`), to obtain the size of
|
||||
the encrypted packet `l`.
|
||||
* A zero-length byte slice is to be passed as the AD (associated data).
|
||||
* The nonce `rn` MUST be incremented after this step.
|
||||
4. Read _exactly_ `l+16` bytes from the network buffer, and let the bytes be
|
||||
known as `c`.
|
||||
5. Decrypt `c` (using `ChaCha20-Poly1305`, `rn`, and `rk`), to obtain decrypted
|
||||
plaintext packet `p`.
|
||||
* The nonce `rn` MUST be incremented after this step.
|
||||
|
||||
==== Lightning Message Key Rotation
|
||||
|
||||
Changing keys regularly and forgetting previous keys is useful to prevent the
|
||||
decryption of old messages, in the case of later key leakage (i.e. backwards
|
||||
secrecy).
|
||||
|
||||
Key rotation is performed for _each_ key (`sk` and `rk`) _individually_. A key
|
||||
is to be rotated after a party encrypts or decrypts 1000 times with it (i.e.
|
||||
every 500 messages). 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 steps:
|
||||
|
||||
1. Let `ck` be the chaining key obtained at the end of Act Three.
|
||||
2. `ck', k' = HKDF(ck, k)`
|
||||
3. Reset the nonce for the key to `n = 0`.
|
||||
4. `k = k'`
|
||||
5. `ck = ck'`
|
Loading…
Reference in New Issue
Block a user