commit ded99436cafc822737a4dcdc0001ff259885a785
parent a1c088c138baf941d603c055ae660868522b7b79
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 14:05:18 +0400
lib: make Handshake abstract and warn against re-reading it
The Handshake always yields the initial Sender and Receiver, so a
caller that keeps it around and calls handshake_sender per message
reuses nonces. Its accessors are now plain functions documented as
single-use, and the type is abstract, so it can't be forged or record
updated. Also notes that encrypt's InvalidLength advances nothing and
isn't fatal.
Diffstat:
1 file changed, 41 insertions(+), 22 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT8.hs b/lib/Lightning/Protocol/BOLT8.hs
@@ -77,7 +77,8 @@
--
-- * __Any error is fatal.__ BOLT #8 requires terminating the
-- connection on any handshake or transport failure. Don't retry with
--- the previous state.
+-- the previous state. The one exception is 'encrypt' rejecting an
+-- oversized message, which advances nothing.
module Lightning.Protocol.BOLT8 (
-- * Keys
@@ -89,7 +90,10 @@ module Lightning.Protocol.BOLT8 (
, serialize_pub
-- * Handshake
- , Handshake(..)
+ , Handshake
+ , handshake_sender
+ , handshake_receiver
+ , handshake_remote_static
-- ** Initiator
, Initiator
@@ -233,19 +237,39 @@ data Responder = Responder
instance NFData Responder where
rnf (Responder h ck k (Sec e)) = h `seq` ck `seq` k `seq` e `seq` ()
--- | The result of a successful handshake.
-data Handshake = Handshake {
- handshake_sender :: !Sender
- -- ^ state for encrypting outgoing messages
- , handshake_receiver :: !Receiver
- -- ^ state for decrypting incoming messages
- , handshake_remote_static :: !Pub
- -- ^ the remote party's authenticated static public key
- }
+-- | The result of a successful handshake: the initial transport
+-- states and the remote party's authenticated static public key.
+--
+-- Take the t'Sender' and t'Receiver' out once, right after the
+-- handshake, and don't keep the t'Handshake' around: it always yields
+-- the /initial/ states, and encrypting with the initial t'Sender'
+-- a second time reuses a nonce.
+data Handshake = Handshake !Sender !Receiver !Pub
instance NFData Handshake where
rnf (Handshake s r p) = rnf s `seq` rnf r `seq` rnf p
+-- | The initial t'Sender', for encrypting outgoing messages.
+--
+-- Call this once per handshake. Every call returns the same initial
+-- state, so encrypting with the result of a second call reuses a
+-- nonce.
+handshake_sender :: Handshake -> Sender
+handshake_sender (Handshake s _ _) = s
+
+-- | The initial t'Receiver', for decrypting incoming messages.
+--
+-- Call this once per handshake. Every call returns the same initial
+-- state, so decrypting with the result of a second call accepts
+-- replayed frames.
+handshake_receiver :: Handshake -> Receiver
+handshake_receiver (Handshake _ r _) = r
+
+-- | The remote party's static public key, authenticated by the
+-- handshake. For the initiator, this is the key passed to 'act1'.
+handshake_remote_static :: Handshake -> Pub
+handshake_remote_static (Handshake _ _ p) = p
+
-- transport state ------------------------------------------------------------
-- One direction's cipher state.
@@ -531,11 +555,8 @@ act3 (Initiator h ck e_sec (Keypair s_sec s_pub) rs) msg2 = do
!t = encrypt_with_ad temp_k3 0 h3 BS.empty
!(sk, rk) = mix_key ck2 BS.empty
!msg = BS.cons 0x00 (c3 <> t)
- !hs = Handshake {
- handshake_sender = Sender (Cipher sk 0 ck2)
- , handshake_receiver = Receiver (Cipher rk 0 ck2)
- , handshake_remote_static = rs
- }
+ !hs = Handshake (Sender (Cipher sk 0 ck2))
+ (Receiver (Cipher rk 0 ck2)) rs
pure (msg, hs)
-- | Responder: process act three, completing the handshake and
@@ -563,11 +584,7 @@ finalize (Responder h ck temp_k2 e_sec) msg3 = do
let !(ck1, temp_k3) = mix_key ck se
_ <- note InvalidMAC (decrypt_with_ad temp_k3 0 h1 t)
let !(rk, sk) = mix_key ck1 BS.empty
- pure Handshake {
- handshake_sender = Sender (Cipher sk 0 ck1)
- , handshake_receiver = Receiver (Cipher rk 0 ck1)
- , handshake_remote_static = rs
- }
+ pure (Handshake (Sender (Cipher sk 0 ck1)) (Receiver (Cipher rk 0 ck1)) rs)
-- transport ------------------------------------------------------------------
@@ -602,7 +619,9 @@ step (Cipher k n ck)
-- Encrypting with the same t'Sender' twice reuses a nonce and breaks
-- confidentiality and authenticity.
--
--- Fails only with 'InvalidLength', for an oversized message.
+-- Fails only with 'InvalidLength', for an oversized message. Nothing
+-- is encrypted in that case, so the given t'Sender' remains the
+-- current one and the connection can continue.
--
-- Continuing the module-level example:
--