diff --git a/14_encrypted_transport.asciidoc b/14_encrypted_transport.asciidoc index aed3622..b85ce94 100644 --- a/14_encrypted_transport.asciidoc +++ b/14_encrypted_transport.asciidoc @@ -37,7 +37,7 @@ that requires encrypted communication between two parties. === The Channel Graph as Decentralized Public Key Infrastructure -((("channel graph","decentralized public key infrastructure")))((("Lightning encrypted transport protocol","channel graph as decentralized public key infrastructure")))((("PKI (public key infrastructure)")))((("public key infrastructure (PKI)")))As we learned in <> when discussing multihop forwarding, every node has a long-term +((("channel graph","decentralized public key infrastructure")))((("Lightning encrypted transport protocol","channel graph as decentralized public key infrastructure")))((("PKI (public key infrastructure)")))((("public key infrastructure (PKI)")))As we learned in <>, every node has a long-term identity that is used as the identifier for a vertex during pathfinding 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 @@ -248,37 +248,38 @@ 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 SHA-256 of the compressed format of the +The returned value is the SHA-256 of the compressed format of the generated point. `HKDF(salt,ikm)`:: A function defined in `RFC 5869`, evaluated with a zero-length `info` field. + - ** All invocations of `HKDF` implicitly return 64 bytes of +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` (Internet Engineering Task Force variant) +Where `encrypt` is an evaluation of `ChaCha20-Poly1305` (Internet Engineering Task Force 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) +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 +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()` ++ +Where the object also has a single method: + ** `.serializeCompressed()` `a || b`:: This denotes the concatenation of two byte strings `a` and `b`. @@ -289,13 +290,15 @@ starting state that they'll use to advance the handshake process. To start, both sides need to construct the initial handshake digest `h`. 1. ++h = SHA-256(__protocolName__)++ - * Where ++__protocolName__ = "Noise_XK_secp256k1_ChaChaPoly_SHA256"++ encoded as ++ +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`. ++ +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. @@ -306,12 +309,10 @@ 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())` + 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())` + Bitcoin's compressed format: `h = SHA-256(h || ls.pub.serializeCompressed())` ===== Handshake acts @@ -348,18 +349,23 @@ Sender actions: 1. `e = generateKey()` 2. `h = SHA-256(h || e.pub.serializeCompressed())` - * The newly generated ephemeral key is accumulated into the running ++ +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 ++ +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 ++ +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 plain text. ++ +Where `zero` is a zero-length plain text. 6. `h = SHA-256(h || c)` - * Finally, the generated ciphertext is accumulated into the authenticating ++ +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. @@ -410,18 +416,23 @@ Sender actions: 1. `e = generateKey()` 2. `h = SHA-256(h || e.pub.serializeCompressed())` - * The newly generated ephemeral key is accumulated into the running ++ +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 ++ +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 ++ +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 plain text. ++ +Where `zero` is a zero-length plain text. 6. `h = SHA-256(h || c)` - * Finally, the generated ciphertext is accumulated into the authenticating ++ +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. @@ -429,24 +440,30 @@ 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 ++ +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 ++ +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 ++ +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 ++ +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 ++ +The received ciphertext is mixed into the handshake digest. This step serves to ensure the payload wasn't modified by a MITM. ====== Act Three @@ -470,54 +487,68 @@ 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. ++ +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. ++ +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. ++ +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 plain text. ++ +Where `zero` is a zero-length plain text. 6. `sk, rk = HKDF(ck, zero)` - * Where `zero` is a zero-length plain text, ++ +Where `zero` is a zero-length plain text, `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 ++ +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. ++ +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 ++ +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 ++ +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. ++ +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 ++ +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 plain text, ++ +Where `zero` is a zero-length plain text, `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 ++ +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.(((range="endofrange", startref="ix_14_encrypted_transport-asciidoc6")))(((range="endofrange", startref="ix_14_encrypted_transport-asciidoc5")))(((range="endofrange", startref="ix_14_encrypted_transport-asciidoc4"))) ++ +The sending and receiving nonces are initialized to 0.(((range="endofrange", startref="ix_14_encrypted_transport-asciidoc6")))(((range="endofrange", startref="ix_14_encrypted_transport-asciidoc5")))(((range="endofrange", startref="ix_14_encrypted_transport-asciidoc4"))) ===== Transport message encryption @@ -555,7 +586,8 @@ 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. ++ +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). @@ -566,7 +598,8 @@ completed: * 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 this encrypted ciphertext be known as `c`. - * The nonce `sn` must be incremented after this step. ++ +The nonce `sn` must be incremented after this step. 5. Send `lc || c` over the network buffer. ====== Receiving and decrypting messages @@ -584,7 +617,8 @@ steps are completed: known as `c`. 5. Decrypt `c` (using `ChaCha20-Poly1305`, `rn`, and `rk`) to obtain decrypted plain-text packet `p`. - * The nonce `rn` must be incremented after this step. ++ +The nonce `rn` must be incremented after this step. ===== Lightning message key rotation