ppad-bolt8-0.0.1: Encrypted and authenticated transport per BOLT #8
Copyright(c) 2025 Jared Tobin
LicenseMIT
MaintainerJared Tobin <jared@ppad.tech>
Safe HaskellNone
LanguageHaskell2010

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') <- encrypt session plaintext

-- receiver
(plaintext, session') <- decrypt session 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

Keys

data Sec Source #

Secret key (32 bytes).

Instances

Instances details
Generic Sec Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Associated Types

type Rep Sec 
Instance details

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)))

Methods

from :: Sec -> Rep Sec x #

to :: Rep Sec x -> Sec #

Eq Sec Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Methods

(==) :: Sec -> Sec -> Bool #

(/=) :: Sec -> Sec -> Bool #

type Rep Sec Source # 
Instance details

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)))

data Pub Source #

Compressed public key.

Instances

Instances details
Show Pub Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Methods

showsPrec :: Int -> Pub -> ShowS #

show :: Pub -> String #

showList :: [Pub] -> ShowS #

Eq Pub Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Methods

(==) :: Pub -> Pub -> Bool #

(/=) :: Pub -> Pub -> Bool #

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 length
Nothing

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 length
Nothing

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)

act1 Source #

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

act3 Source #

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)

act2 Source #

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

finalize Source #

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

data Session Source #

Post-handshake session state.

Instances

Instances details
Generic Session Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Associated Types

type Rep Session 
Instance details

Defined in Lightning.Protocol.BOLT8

type Rep Session = D1 ('MetaData "Session" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'False) (C1 ('MetaCons "Session" 'PrefixI 'True) ((S1 ('MetaSel ('Just "sess_sk") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: (S1 ('MetaSel ('Just "sess_sn") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 Word64) :*: S1 ('MetaSel ('Just "sess_sck") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString))) :*: (S1 ('MetaSel ('Just "sess_rk") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: (S1 ('MetaSel ('Just "sess_rn") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 Word64) :*: S1 ('MetaSel ('Just "sess_rck") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString)))))

Methods

from :: Session -> Rep Session x #

to :: Rep Session x -> Session #

type Rep Session Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

type Rep Session = D1 ('MetaData "Session" "Lightning.Protocol.BOLT8" "ppad-bolt8-0.0.1-CxZeVTtHWfPGrY3iB3s59R" 'False) (C1 ('MetaCons "Session" 'PrefixI 'True) ((S1 ('MetaSel ('Just "sess_sk") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: (S1 ('MetaSel ('Just "sess_sn") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 Word64) :*: S1 ('MetaSel ('Just "sess_sck") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString))) :*: (S1 ('MetaSel ('Just "sess_rk") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString) :*: (S1 ('MetaSel ('Just "sess_rn") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 Word64) :*: S1 ('MetaSel ('Just "sess_rck") 'SourceUnpack 'SourceStrict 'DecidedStrict) (Rec0 ByteString)))))

data HandshakeState Source #

Internal handshake state (exported for benchmarking).

Instances

Instances details
Generic HandshakeState Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

type Rep HandshakeState Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

data Handshake Source #

Result of a successful handshake.

Constructors

Handshake 

Fields

Instances

Instances details
Generic Handshake Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Associated Types

type Rep Handshake 
Instance details

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)))
type Rep Handshake Source # 
Instance details

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)))

encrypt Source #

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

decrypt Source #

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"

decrypt_frame Source #

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 NeedMore n where n is the bytes still needed.
  • If the length header is complete but the body is incomplete, returns NeedMore n with bytes needed for the full frame.
  • MAC or decryption failures return FrameError.
  • A complete, valid frame returns FrameOk with 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 Int is the minimum additional bytes required.

FrameOk !ByteString !ByteString !Session

Successfully decrypted: plaintext, remainder, updated session.

FrameError !Error

Decryption failed with the given error.

Instances

Instances details
Generic FrameResult Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

type Rep FrameResult Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Errors

data Error Source #

Handshake errors.

Instances

Instances details
Generic Error Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Associated Types

type Rep Error 
Instance details

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))))

Methods

from :: Error -> Rep Error x #

to :: Rep Error x -> Error #

Show Error Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Methods

showsPrec :: Int -> Error -> ShowS #

show :: Error -> String #

showList :: [Error] -> ShowS #

Eq Error Source # 
Instance details

Defined in Lightning.Protocol.BOLT8

Methods

(==) :: Error -> Error -> Bool #

(/=) :: Error -> Error -> Bool #

type Rep Error Source # 
Instance details

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))))