| Copyright | (c) 2025 Jared Tobin |
|---|---|
| License | MIT |
| Maintainer | Jared Tobin <jared@ppad.tech> |
| Safe Haskell | None |
| Language | Haskell2010 |
Lightning.Protocol.BOLT8
Description
Encrypted and authenticated transport for the Lightning Network, per BOLT #8.
This module implements the Noise_XK_secp256k1_ChaChaPoly_SHA256 handshake and subsequent encrypted message transport.
Handshake
A BOLT #8 handshake consists of three acts. The initiator knows the responder's static public key in advance and initiates the connection:
(msg1, state) <- act1 i_sec i_pub r_pub entropy
-- send msg1 (50 bytes) to responder
-- receive msg2 (50 bytes) from responder
(msg3, result) <- act3 state msg2
-- send msg3 (66 bytes) to responder
let session = session result
The responder receives the connection and authenticates the initiator:
-- receive msg1 (50 bytes) from initiator
(msg2, state) <- act2 r_sec r_pub entropy msg1
-- send msg2 (50 bytes) to initiator
-- receive msg3 (66 bytes) from initiator
result <- finalize state msg3
let session = session result
Message Transport
After a successful handshake, use encrypt and decrypt to exchange
messages. Each returns an updated Session that must be used for the
next operation (keys rotate every 1000 messages):
-- sender (ciphertext, session') <-encryptsession plaintext -- receiver (plaintext, session') <-decryptsession ciphertext
Message Framing
BOLT #8 runs over a byte stream, so callers often need to deal with
partial buffers. Use decrypt_frame when you have exactly one frame,
or decrypt_frame_partial to handle incremental reads and return how
many bytes are still needed.
Maximum plaintext size is 65535 bytes.
Synopsis
- data Sec
- data Pub
- keypair :: ByteString -> Maybe (Sec, Pub)
- parse_pub :: ByteString -> Maybe Pub
- serialize_pub :: Pub -> ByteString
- act1 :: Sec -> Pub -> Pub -> ByteString -> Either Error (ByteString, HandshakeState)
- act3 :: HandshakeState -> ByteString -> Either Error (ByteString, Handshake)
- act2 :: Sec -> Pub -> ByteString -> ByteString -> Either Error (ByteString, HandshakeState)
- finalize :: HandshakeState -> ByteString -> Either Error Handshake
- data Session
- data HandshakeState
- data Handshake = Handshake {
- session :: !Session
- remote_static :: !Pub
- encrypt :: Session -> ByteString -> Either Error (ByteString, Session)
- decrypt :: Session -> ByteString -> Either Error (ByteString, Session)
- decrypt_frame :: Session -> ByteString -> Either Error (ByteString, ByteString, Session)
- decrypt_frame_partial :: Session -> ByteString -> FrameResult
- data FrameResult
- = NeedMore !Int
- | FrameOk !ByteString !ByteString !Session
- | FrameError !Error
- data Error
Keys
Secret key (32 bytes).
Instances
| Generic Sec Source # | |||||
Defined in Lightning.Protocol.BOLT8 Associated Types
| |||||
| Eq Sec Source # | |||||
| type Rep Sec Source # | |||||
Defined in Lightning.Protocol.BOLT8 type Rep Sec = D1 ('MetaData "Sec" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'True) (C1 ('MetaCons "Sec" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 ByteString))) | |||||
Compressed public key.
keypair :: ByteString -> Maybe (Sec, Pub) Source #
Derive a keypair from 32 bytes of entropy.
Returns Nothing if the entropy is invalid (zero or >= curve order).
>>>let ent = BS.replicate 32 0x11>>>case keypair ent of { Just _ -> "ok"; Nothing -> "fail" }"ok">>>keypair (BS.replicate 31 0x11) -- wrong lengthNothing
parse_pub :: ByteString -> Maybe Pub Source #
Parse a 33-byte compressed public key.
>>>let Just (_, pub) = keypair (BS.replicate 32 0x11)>>>let bytes = serialize_pub pub>>>case parse_pub bytes of { Just _ -> "ok"; Nothing -> "fail" }"ok">>>parse_pub (BS.replicate 32 0x00) -- wrong lengthNothing
serialize_pub :: Pub -> ByteString Source #
Serialize a public key to 33-byte compressed form.
>>>let Just (_, pub) = keypair (BS.replicate 32 0x11)>>>BS.length (serialize_pub pub)33
Handshake (initiator)
Arguments
| :: Sec | local static secret |
| -> Pub | local static public |
| -> Pub | remote static public (responder's) |
| -> ByteString | 32 bytes entropy for ephemeral |
| -> Either Error (ByteString, HandshakeState) |
Initiator: generate Act 1 message (50 bytes).
Takes local static key, remote static pubkey, and 32 bytes of entropy for ephemeral key generation.
Returns the 50-byte Act 1 message and handshake state for Act 3.
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let eph_ent = BS.replicate 32 0x12>>>case act1 i_sec i_pub r_pub eph_ent of { Right (msg, _) -> BS.length msg; Left _ -> 0 }50
Arguments
| :: HandshakeState | state after Act 1 |
| -> ByteString | Act 2 message (50 bytes) |
| -> Either Error (ByteString, Handshake) |
Initiator: process Act 2 and generate Act 3 (66 bytes), completing the handshake.
Returns the 66-byte Act 3 message and the handshake result.
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let Right (msg1, i_hs) = act1 i_sec i_pub r_pub (BS.replicate 32 0x12)>>>let Right (msg2, _) = act2 r_sec r_pub (BS.replicate 32 0x22) msg1>>>case act3 i_hs msg2 of { Right (msg, _) -> BS.length msg; Left _ -> 0 }66
Handshake (responder)
Arguments
| :: Sec | local static secret |
| -> Pub | local static public |
| -> ByteString | 32 bytes entropy for ephemeral |
| -> ByteString | Act 1 message (50 bytes) |
| -> Either Error (ByteString, HandshakeState) |
Responder: process Act 1 and generate Act 2 message (50 bytes).
Takes local static key and 32 bytes of entropy for ephemeral key, plus the 50-byte Act 1 message from initiator.
Returns the 50-byte Act 2 message and handshake state for finalize.
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let Right (msg1, _) = act1 i_sec i_pub r_pub (BS.replicate 32 0x12)>>>case act2 r_sec r_pub (BS.replicate 32 0x22) msg1 of { Right (msg, _) -> BS.length msg; Left _ -> 0 }50
Arguments
| :: HandshakeState | state after Act 2 |
| -> ByteString | Act 3 message (66 bytes) |
| -> Either Error Handshake |
Responder: process Act 3 (66 bytes) and complete the handshake.
Returns the handshake result with authenticated remote static pubkey.
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let Right (msg1, i_hs) = act1 i_sec i_pub r_pub (BS.replicate 32 0x12)>>>let Right (msg2, r_hs) = act2 r_sec r_pub (BS.replicate 32 0x22) msg1>>>let Right (msg3, _) = act3 i_hs msg2>>>case finalize r_hs msg3 of { Right _ -> "ok"; Left e -> show e }"ok"
Session
Post-handshake session state.
Instances
data HandshakeState Source #
Internal handshake state (exported for benchmarking).
Instances
| Generic HandshakeState Source # | |||||
Defined in Lightning.Protocol.BOLT8 Associated Types
Methods from :: HandshakeState -> Rep HandshakeState x # to :: Rep HandshakeState x -> HandshakeState # | |||||
| type Rep HandshakeState Source # | |||||
Defined in Lightning.Protocol.BOLT8 type Rep HandshakeState = D1 ('MetaData "HandshakeState" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'False) (C1 ('MetaCons "HandshakeState" 'PrefixI 'True) (((S1 ('MetaSel ('Just "hs_h") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: S1 ('MetaSel ('Just "hs_ck") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString)) :*: (S1 ('MetaSel ('Just "hs_temp_k") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: S1 ('MetaSel ('Just "hs_e_sec") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Sec))) :*: ((S1 ('MetaSel ('Just "hs_e_pub") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Pub) :*: S1 ('MetaSel ('Just "hs_s_sec") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Sec)) :*: (S1 ('MetaSel ('Just "hs_s_pub") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Pub) :*: (S1 ('MetaSel ('Just "hs_re") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 (Maybe Pub)) :*: S1 ('MetaSel ('Just "hs_rs") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 (Maybe Pub))))))) | |||||
Result of a successful handshake.
Constructors
| Handshake | |
Fields
| |
Instances
| Generic Handshake Source # | |||||
Defined in Lightning.Protocol.BOLT8 Associated Types
| |||||
| type Rep Handshake Source # | |||||
Defined in Lightning.Protocol.BOLT8 type Rep Handshake = D1 ('MetaData "Handshake" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'False) (C1 ('MetaCons "Handshake" 'PrefixI 'True) (S1 ('MetaSel ('Just "session") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Session) :*: S1 ('MetaSel ('Just "remote_static") 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Pub))) | |||||
Arguments
| :: Session | |
| -> ByteString | plaintext (max 65535 bytes) |
| -> Either Error (ByteString, Session) |
Encrypt a message (max 65535 bytes).
Returns the encrypted packet and updated session. Key rotation is handled automatically at nonce 1000.
Wire format: encrypted_length (2) || MAC (16) || encrypted_body || MAC (16)
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let Right (msg1, i_hs) = act1 i_sec i_pub r_pub (BS.replicate 32 0x12)>>>let Right (msg2, _) = act2 r_sec r_pub (BS.replicate 32 0x22) msg1>>>let Right (_, i_result) = act3 i_hs msg2>>>let sess = session i_result>>>case encrypt sess "hello" of { Right (ct, _) -> BS.length ct; Left _ -> 0 }39
Arguments
| :: Session | |
| -> ByteString | encrypted packet (exact length required) |
| -> Either Error (ByteString, Session) |
Decrypt a message, requiring an exact packet with no trailing bytes.
Returns the plaintext and updated session. Key rotation is handled automatically at nonce 1000.
This is a strict variant that rejects any trailing data. For
streaming use cases where you need to handle multiple frames in a
buffer, use decrypt_frame instead.
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let Right (msg1, i_hs) = act1 i_sec i_pub r_pub (BS.replicate 32 0x12)>>>let Right (msg2, r_hs) = act2 r_sec r_pub (BS.replicate 32 0x22) msg1>>>let Right (msg3, i_result) = act3 i_hs msg2>>>let Right r_result = finalize r_hs msg3>>>let Right (ct, _) = encrypt (session i_result) "hello">>>case decrypt (session r_result) ct of { Right (pt, _) -> pt; Left _ -> "fail" }"hello"
Arguments
| :: Session | |
| -> ByteString | buffer containing at least one encrypted frame |
| -> Either Error (ByteString, ByteString, Session) |
Decrypt a single frame from a buffer, returning the remainder.
Returns the plaintext, any unconsumed bytes, and the updated session. Key rotation is handled automatically every 1000 messages.
This is useful for streaming scenarios where multiple messages may
be buffered together. The remainder can be passed to the next call
to decrypt_frame.
Wire format consumed: encrypted_length (18) || encrypted_body (len + 16)
>>>let Just (i_sec, i_pub) = keypair (BS.replicate 32 0x11)>>>let Just (r_sec, r_pub) = keypair (BS.replicate 32 0x21)>>>let Right (msg1, i_hs) = act1 i_sec i_pub r_pub (BS.replicate 32 0x12)>>>let Right (msg2, r_hs) = act2 r_sec r_pub (BS.replicate 32 0x22) msg1>>>let Right (msg3, i_result) = act3 i_hs msg2>>>let Right r_result = finalize r_hs msg3>>>let Right (ct, _) = encrypt (session i_result) "hello">>>case decrypt_frame (session r_result) ct of { Right (pt, rem, _) -> (pt, BS.null rem); Left _ -> ("fail", False) }("hello",True)
decrypt_frame_partial Source #
Arguments
| :: Session | |
| -> ByteString | buffer (possibly incomplete) |
| -> FrameResult |
Decrypt a frame from a partial buffer, indicating when more data needed.
Unlike decrypt_frame, this function handles incomplete buffers
gracefully by returning NeedMore with the number of additional
bytes required to make progress.
- If the buffer has fewer than 18 bytes (encrypted length + MAC),
returns
whereNeedMorennis the bytes still needed. - If the length header is complete but the body is incomplete,
returns
with bytes needed for the full frame.NeedMoren - MAC or decryption failures return
FrameError. - A complete, valid frame returns
FrameOkwith plaintext, remainder, and updated session.
This is useful for non-blocking I/O where data arrives incrementally.
data FrameResult Source #
Result of attempting to decrypt a frame from a partial buffer.
Constructors
| NeedMore !Int | More bytes needed; the |
| FrameOk !ByteString !ByteString !Session | Successfully decrypted: plaintext, remainder, updated session. |
| FrameError !Error | Decryption failed with the given error. |
Instances
| Generic FrameResult Source # | |||||
Defined in Lightning.Protocol.BOLT8 Associated Types
| |||||
| type Rep FrameResult Source # | |||||
Defined in Lightning.Protocol.BOLT8 type Rep FrameResult = D1 ('MetaData "FrameResult" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'False) (C1 ('MetaCons "NeedMore" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 Int)) :+: (C1 ('MetaCons "FrameOk" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Session))) :+: C1 ('MetaCons "FrameError" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'SourceStrict 'DecidedStrict) (Rec0 Error)))) | |||||
Errors
Handshake errors.
Instances
| Generic Error Source # | |||||
Defined in Lightning.Protocol.BOLT8 Associated Types
| |||||
| Show Error Source # | |||||
| Eq Error Source # | |||||
| type Rep Error Source # | |||||
Defined in Lightning.Protocol.BOLT8 type Rep Error = D1 ('MetaData "Error" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'False) ((C1 ('MetaCons "InvalidKey" 'PrefixI 'False) (U1 :: Type -> Type) :+: (C1 ('MetaCons "InvalidPub" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "InvalidMAC" 'PrefixI 'False) (U1 :: Type -> Type))) :+: (C1 ('MetaCons "InvalidVersion" 'PrefixI 'False) (U1 :: Type -> Type) :+: (C1 ('MetaCons "InvalidLength" 'PrefixI 'False) (U1 :: Type -> Type) :+: C1 ('MetaCons "DecryptionFailed" 'PrefixI 'False) (U1 :: Type -> Type)))) | |||||