packages feed

ppad-bolt4-0.0.1: lib/Lightning/Protocol/BOLT4/Prim.hs

{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module: Lightning.Protocol.BOLT4.Prim
-- Copyright: (c) 2025 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Low-level cryptographic primitives for BOLT4 onion routing.

module Lightning.Protocol.BOLT4.Prim (
    -- * Types
    SharedSecret(..)
  , DerivedKey(..)
  , BlindingFactor(..)

    -- * Key derivation
  , deriveRho
  , deriveMu
  , deriveUm
  , derivePad
  , deriveAmmag

    -- * Shared secret computation
  , computeSharedSecret

    -- * Blinding factor computation
  , computeBlindingFactor

    -- * Key blinding
  , blindPubKey
  , blindSecKey

    -- * Stream generation
  , generateStream

    -- * HMAC operations
  , computeHmac
  , verifyHmac
  ) where

import qualified Crypto.Cipher.ChaCha20 as ChaCha
import qualified Crypto.Curve.Secp256k1 as Secp256k1
import qualified Crypto.Hash.SHA256 as SHA256
import Data.Bits (xor)
import qualified Data.ByteString as BS
import qualified Data.List as L
import Data.Word (Word8, Word32)
import qualified Numeric.Montgomery.Secp256k1.Scalar as S

-- | 32-byte shared secret derived from ECDH.
newtype SharedSecret = SharedSecret BS.ByteString
  deriving (Eq, Show)

-- | 32-byte derived key (rho, mu, um, pad, ammag).
newtype DerivedKey = DerivedKey BS.ByteString
  deriving (Eq, Show)

-- | 32-byte blinding factor for ephemeral key updates.
newtype BlindingFactor = BlindingFactor BS.ByteString
  deriving (Eq, Show)

-- Key derivation ------------------------------------------------------------

-- | Derive rho key for obfuscation stream generation.
--
-- @rho = HMAC-SHA256(key="rho", data=shared_secret)@
deriveRho :: SharedSecret -> DerivedKey
deriveRho = deriveKey "rho"
{-# INLINE deriveRho #-}

-- | Derive mu key for HMAC computation.
--
-- @mu = HMAC-SHA256(key="mu", data=shared_secret)@
deriveMu :: SharedSecret -> DerivedKey
deriveMu = deriveKey "mu"
{-# INLINE deriveMu #-}

-- | Derive um key for return error HMAC.
--
-- @um = HMAC-SHA256(key="um", data=shared_secret)@
deriveUm :: SharedSecret -> DerivedKey
deriveUm = deriveKey "um"
{-# INLINE deriveUm #-}

-- | Derive pad key for filler generation.
--
-- @pad = HMAC-SHA256(key="pad", data=shared_secret)@
derivePad :: SharedSecret -> DerivedKey
derivePad = deriveKey "pad"
{-# INLINE derivePad #-}

-- | Derive ammag key for error obfuscation.
--
-- @ammag = HMAC-SHA256(key="ammag", data=shared_secret)@
deriveAmmag :: SharedSecret -> DerivedKey
deriveAmmag = deriveKey "ammag"
{-# INLINE deriveAmmag #-}

-- Internal helper for key derivation.
deriveKey :: BS.ByteString -> SharedSecret -> DerivedKey
deriveKey !keyType (SharedSecret !ss) =
  let SHA256.MAC !result = SHA256.hmac keyType ss
  in  DerivedKey result
{-# INLINE deriveKey #-}

-- Shared secret computation -------------------------------------------------

-- | Compute shared secret from ECDH.
--
-- Takes a 32-byte secret key and a public key.
-- Returns SHA256 of the compressed ECDH point (33 bytes).
computeSharedSecret
  :: BS.ByteString         -- ^ 32-byte secret key
  -> Secp256k1.Projective  -- ^ public key
  -> Maybe SharedSecret
computeSharedSecret !secBs !pub = do
  sec <- Secp256k1.roll32 secBs
  ecdhPoint <- Secp256k1.mul pub sec
  let !compressed = Secp256k1.serialize_point ecdhPoint
      !ss = SHA256.hash compressed
  pure $! SharedSecret ss
{-# INLINE computeSharedSecret #-}

-- Blinding factor -----------------------------------------------------------

-- | Compute blinding factor for ephemeral key updates.
--
-- @blinding_factor = SHA256(ephemeral_pubkey || shared_secret)@
computeBlindingFactor
  :: Secp256k1.Projective  -- ^ ephemeral public key
  -> SharedSecret          -- ^ shared secret
  -> BlindingFactor
computeBlindingFactor !pub (SharedSecret !ss) =
  let !pubBytes = Secp256k1.serialize_point pub
      !combined = pubBytes <> ss
      !hashed = SHA256.hash combined
  in  BlindingFactor hashed
{-# INLINE computeBlindingFactor #-}

-- Key blinding --------------------------------------------------------------

-- | Blind a public key by multiplying with blinding factor.
--
-- @new_pubkey = pubkey * blinding_factor@
blindPubKey
  :: Secp256k1.Projective
  -> BlindingFactor
  -> Maybe Secp256k1.Projective
blindPubKey !pub (BlindingFactor !bf) = do
  sk <- Secp256k1.roll32 bf
  Secp256k1.mul pub sk
{-# INLINE blindPubKey #-}

-- | Blind a secret key by multiplying with blinding factor (mod curve order).
--
-- @new_seckey = seckey * blinding_factor (mod q)@
--
-- Uses Montgomery multiplication from ppad-fixed for efficiency.
-- Takes a 32-byte secret key and returns a 32-byte blinded secret key.
blindSecKey
  :: BS.ByteString     -- ^ 32-byte secret key
  -> BlindingFactor    -- ^ blinding factor
  -> Maybe BS.ByteString  -- ^ 32-byte blinded secret key
blindSecKey !secBs (BlindingFactor !bf)
  | BS.length secBs /= 32 = Nothing
  | BS.length bf /= 32 = Nothing
  | otherwise =
      let !secW = Secp256k1.unsafe_roll32 secBs
          !bfW = Secp256k1.unsafe_roll32 bf
          !secM = S.to secW
          !bfM = S.to bfW
          !resultM = S.mul secM bfM
          !resultW = S.retr resultM
      in  Just $! Secp256k1.unroll32 resultW
{-# INLINE blindSecKey #-}

-- Stream generation ---------------------------------------------------------

-- | Generate pseudo-random byte stream using ChaCha20.
--
-- Uses derived key as ChaCha20 key, 96-bit zero nonce, counter=0.
-- Encrypts zeros to produce keystream.
generateStream
  :: DerivedKey     -- ^ rho or ammag key
  -> Int            -- ^ desired length
  -> BS.ByteString
generateStream (DerivedKey !key) !len =
  let !nonce = BS.replicate 12 0
      !zeros = BS.replicate len 0
  in  either (const (BS.replicate len 0)) id
        (ChaCha.cipher key (0 :: Word32) nonce zeros)
{-# INLINE generateStream #-}

-- HMAC operations -----------------------------------------------------------

-- | Compute HMAC-SHA256 for packet integrity.
computeHmac
  :: DerivedKey      -- ^ mu key
  -> BS.ByteString   -- ^ hop_payloads
  -> BS.ByteString   -- ^ associated_data
  -> BS.ByteString   -- ^ 32-byte HMAC
computeHmac (DerivedKey !key) !payloads !assocData =
  let SHA256.MAC !result = SHA256.hmac key (payloads <> assocData)
  in  result
{-# INLINE computeHmac #-}

-- | Constant-time HMAC comparison.
verifyHmac
  :: BS.ByteString  -- ^ expected
  -> BS.ByteString  -- ^ computed
  -> Bool
verifyHmac !expected !computed
  | BS.length expected /= BS.length computed = False
  | otherwise = constantTimeEq expected computed
{-# INLINE verifyHmac #-}

-- Constant-time equality comparison.
constantTimeEq :: BS.ByteString -> BS.ByteString -> Bool
constantTimeEq !a !b =
  let !diff = L.foldl' (\acc (x, y) -> acc `xor` (x `xor` y)) (0 :: Word8)
                       (BS.zip a b)
  in  diff == 0
{-# INLINE constantTimeEq #-}