bolt8

Encrypted and authenticated transport, per BOLT #8 (docs.ppad.tech/bolt8).
git clone git://git.ppad.tech/bolt8.git
Log | Files | Refs | README | LICENSE

commit aeed9c7a24b3fc4d51d0240b005a36ee1bc531da
parent 05f1c4c421269b4dda6592b1b9928f42e01deaaa
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 13:39:19 +0400

test: cover all Appendix A vectors via the public API

- All 13 handshake failure vectors, asserted to their exact errors.
- Final sk, rk and ck on both sides, checked against frames built
  independently with ppad-aead and ppad-hkdf, across two rotations in
  each direction; the reference rotation is checked against the
  spec's intermediate keys.
- Message vectors 0, 1, 500, 501, 1000 and 1001, and the responder
  decrypting the full spec stream.
- Negative transport tests: bad length MAC, tampered length, bad body
  MAC, truncated and over-long frames, replayed, reordered and
  reflected frames, and oversized messages.
- Properties: handshake key agreement in both directions for random
  keys, round trips up to 65535 bytes, and header/body decryption of
  random frame streams.

Test helpers are total, and the suite no longer builds with -O2.

Diffstat:
Mppad-bolt8.cabal | 6++++--
Mtest/Main.hs | 1423++++++++++++++++++++++++++++++++++++++-----------------------------------------
2 files changed, 686 insertions(+), 743 deletions(-)

diff --git a/ppad-bolt8.cabal b/ppad-bolt8.cabal @@ -48,14 +48,16 @@ test-suite bolt8-tests main-is: Main.hs ghc-options: - -rtsopts -Wall -O2 + -rtsopts -Wall build-depends: base , bytestring + , ppad-aead , ppad-base16 , ppad-bolt8 - , QuickCheck + , ppad-hkdf + , ppad-sha256 , tasty , tasty-hunit , tasty-quickcheck diff --git a/test/Main.hs b/test/Main.hs @@ -1,791 +1,732 @@ -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module Main where -import Data.Bits (xor) +import qualified Crypto.AEAD.ChaCha20Poly1305 as AEAD +import qualified Crypto.Hash.SHA256 as SHA256 +import qualified Crypto.KDF.HMAC as HKDF +import Data.Bits (shiftR, xor) import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 +import Data.Maybe (isJust, isNothing) +import Data.Word (Word64) import qualified Lightning.Protocol.BOLT8 as BOLT8 -import qualified Lightning.Protocol.BOLT8.Internal as I import Test.Tasty import Test.Tasty.HUnit -import Test.Tasty.QuickCheck (Gen, Property, choose, forAll, - testProperty, vectorOf, (===), - (.&&.)) - --- test helpers ---------------------------------------------------------------- - --- | Extract a Just value or fail the test. -expectJust :: String -> Maybe a -> IO a -expectJust msg = \case - Nothing -> assertFailure msg >> error "unreachable" - Just a -> pure a - --- | Extract a Right value or fail the test. -expectRight :: Show e => String -> Either e a -> IO a -expectRight msg = \case - Left e -> assertFailure (msg ++ ": " ++ show e) >> error "unreachable" - Right a -> pure a +import qualified Test.Tasty.QuickCheck as Q main :: IO () main = defaultMain $ testGroup "ppad-bolt8" [ - handshake_tests + key_tests + , handshake_tests + , handshake_failure_tests , message_tests - , framing_tests - , partial_framing_tests - , negative_tests - , rotation_tests + , transport_tests + , transport_failure_tests , property_tests ] --- test vectors from BOLT #8 specification ----------------------------------- +-- helpers -------------------------------------------------------------------- --- initiator static private key -initiator_s_priv :: BS.ByteString -initiator_s_priv = hex - "1111111111111111111111111111111111111111111111111111111111111111" +expect_just :: String -> Maybe a -> IO a +expect_just msg = maybe (assertFailure msg) pure --- initiator ephemeral private key -initiator_e_priv :: BS.ByteString -initiator_e_priv = hex - "1212121212121212121212121212121212121212121212121212121212121212" +expect_right :: Show e => String -> Either e a -> IO a +expect_right msg = either (\e -> assertFailure (msg <> ": " <> show e)) pure --- responder static private key -responder_s_priv :: BS.ByteString -responder_s_priv = hex - "2121212121212121212121212121212121212121212121212121212121212121" +-- assert failure with a specific error +expect_error :: String -> BOLT8.Error -> Either BOLT8.Error a -> Assertion +expect_error msg want r = case r of + Left got -> assertEqual msg want got + Right _ -> assertFailure (msg <> ": expected " <> show want) --- responder static public key (known to initiator) -responder_s_pub :: BS.ByteString -responder_s_pub = hex - "028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f7" +-- decode a hex literal, failing the test if it is malformed +unhex :: BS.ByteString -> IO BS.ByteString +unhex h = expect_just ("invalid hex: " <> show h) (B16.decode h) + +-- total lookup +at :: Int -> [a] -> Maybe a +at i xs = case drop i xs of + (x : _) | i >= 0 -> Just x + _ -> Nothing + +-- flip all bits of the byte at the given index +flip_byte :: Int -> BS.ByteString -> IO BS.ByteString +flip_byte i bs + | i < 0 || i >= BS.length bs = + assertFailure ("flip_byte: index out of bounds: " <> show i) + | otherwise = + let (pre, post) = BS.splitAt i bs + in pure (pre <> BS.map (xor 0xff) (BS.take 1 post) <> BS.drop 1 post) + +-- payload for the m'th message of a test stream +payload :: Int -> BS.ByteString +payload m = BS.replicate (m `mod` 7) (fromIntegral m) + +-- Appendix A key material ---------------------------------------------------- --- responder ephemeral private key -responder_e_priv :: BS.ByteString -responder_e_priv = hex - "2222222222222222222222222222222222222222222222222222222222222222" +ls_i_sec, e_i_sec, ls_r_sec, e_r_sec :: BS.ByteString +ls_i_sec = BS.replicate 32 0x11 +e_i_sec = BS.replicate 32 0x12 +ls_r_sec = BS.replicate 32 0x21 +e_r_sec = BS.replicate 32 0x22 --- expected act 1 message -expected_act1 :: BS.ByteString -expected_act1 = hex +ls_i_pub, ls_r_pub, e_i_pub, e_r_pub :: BS.ByteString +ls_i_pub = + "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" +ls_r_pub = + "028d7500dd4c12685d1f568b4c2b5048e8534b873319f3a8daa612b469132ec7f7" +e_i_pub = + "036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f7" +e_r_pub = + "02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27" + +act1_msg, act2_msg, act3_msg :: BS.ByteString +act1_msg = "00036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f7\ \0df6086551151f58b8afe6c195782c6a" - --- expected act 2 message -expected_act2 :: BS.ByteString -expected_act2 = hex +act2_msg = "0002466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27\ \6e2470b93aac583c9ef6eafca3f730ae" - --- expected act 3 message -expected_act3 :: BS.ByteString -expected_act3 = hex +act3_msg = "00b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355\ \361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139ba" --- handshake tests ----------------------------------------------------------- +-- final chaining key, and the initiator's sending and receiving keys +-- (the responder's are swapped) +final_ck, final_sk, final_rk :: BS.ByteString +final_ck = + "919219dbb2920afa8db80f9a51787a840bcf111ed8d588caf9ab4be716e42b01" +final_sk = + "969ab31b4d288cedf6218839b27a3e2140827047f2c0f01bf5c04435d43511a9" +final_rk = + "bb9020b8965f4df047e07f955f3c4b88418984aadc5cdb35096b9ea8fa5c3442" + +spec_keypairs :: IO (BOLT8.Keypair, BOLT8.Keypair) +spec_keypairs = do + i <- expect_just "initiator keypair" (BOLT8.keypair ls_i_sec) + r <- expect_just "responder keypair" (BOLT8.keypair ls_r_sec) + pure (i, r) + +-- the Appendix A handshake, as (initiator, responder) results +spec_handshake :: Either BOLT8.Error (BOLT8.Handshake, BOLT8.Handshake) +spec_handshake = do + i <- maybe (Left BOLT8.InvalidEntropy) Right (BOLT8.keypair ls_i_sec) + r <- maybe (Left BOLT8.InvalidEntropy) Right (BOLT8.keypair ls_r_sec) + handshake i r e_i_sec e_r_sec + +spec_results :: IO (BOLT8.Handshake, BOLT8.Handshake) +spec_results = expect_right "spec handshake" spec_handshake + +handshake + :: BOLT8.Keypair + -> BOLT8.Keypair + -> BS.ByteString + -> BS.ByteString + -> Either BOLT8.Error (BOLT8.Handshake, BOLT8.Handshake) +handshake i r i_e r_e = do + (msg1, i_hs) <- BOLT8.act1 i (BOLT8.keypair_pub r) i_e + (msg2, r_hs) <- BOLT8.act2 r r_e msg1 + (msg3, i_res) <- BOLT8.act3 i_hs msg2 + r_res <- BOLT8.finalize r_hs msg3 + pure (i_res, r_res) + +-- reference transport -------------------------------------------------------- + +-- BOLT #8 framing and rotation built directly on ppad-aead and +-- ppad-hkdf, as an independent check of the library's key schedule. + +ref_nonce :: Word64 -> BS.ByteString +ref_nonce n = BS.replicate 4 0 <> + BS.pack [fromIntegral (n `shiftR` (8 * j)) | j <- [0 .. 7]] + +ref_frame :: BS.ByteString -> Word64 -> BS.ByteString -> IO BS.ByteString +ref_frame k n m = do + let len = BS.length m + l = BS.pack [fromIntegral (len `shiftR` 8), fromIntegral len] + (lc, lt) <- expect_right "aead" (AEAD.encrypt mempty k (ref_nonce n) l) + (c, t) <- expect_right "aead" (AEAD.encrypt mempty k (ref_nonce (n + 1)) m) + pure (lc <> lt <> c <> t) + +-- (ck', k') = HKDF(ck, k) +ref_rotate + :: BS.ByteString -> BS.ByteString -> IO (BS.ByteString, BS.ByteString) +ref_rotate ck k = do + out <- expect_just "hkdf" (HKDF.derive hmac ck mempty 64 k) + pure (BS.splitAt 32 out) + where + hmac a b = case SHA256.hmac a b of SHA256.MAC mac -> mac + +-- frames carrying 'payload' 0 .. count - 1, sent under key k and +-- chaining key ck +ref_stream :: BS.ByteString -> BS.ByteString -> Int -> IO [BS.ByteString] +ref_stream = go 0 0 + where + go m n ck k count + | m == count = pure [] + | n >= 1000 = do + (ck', k') <- ref_rotate ck k + go m 0 ck' k' count + | otherwise = do + f <- ref_frame k n (payload m) + fs <- go (m + 1) (n + 2) ck k count + pure (f : fs) + +-- assert that a Sender encrypts under key k with chaining key ck, +-- across two rotations +check_sender :: BS.ByteString -> BS.ByteString -> BOLT8.Sender -> Assertion +check_sender ck k s0 = do + ref <- ref_stream ck k 1002 + let go _ _ [] = pure () + go m s (f : fs) = do + (ct, s') <- expect_right "encrypt" (BOLT8.encrypt s (payload m)) + assertEqual ("frame " <> show m) f ct + go (m + 1) s' fs + go (0 :: Int) s0 ref + +-- assert that a Receiver decrypts under key k with chaining key ck, +-- across two rotations +check_receiver + :: BS.ByteString -> BS.ByteString -> BOLT8.Receiver -> Assertion +check_receiver ck k r0 = do + ref <- ref_stream ck k 1002 + let go _ _ [] = pure () + go m r (f : fs) = do + (pt, r') <- expect_right ("decrypt " <> show m) (BOLT8.decrypt r f) + assertEqual ("message " <> show m) (payload m) pt + go (m + 1) r' fs + go (0 :: Int) r0 ref + +-- send 'payload' 0 .. count - 1 from a Sender to a Receiver +round_trip :: Int -> BOLT8.Sender -> BOLT8.Receiver -> Assertion +round_trip count = go 0 + where + go m s r + | m == count = pure () + | otherwise = do + (ct, s') <- expect_right "encrypt" (BOLT8.encrypt s (payload m)) + (pt, r') <- expect_right ("decrypt " <> show m) (BOLT8.decrypt r ct) + assertEqual ("message " <> show m) (payload m) pt + go (m + 1) s' r' + +-- keys ----------------------------------------------------------------------- + +key_tests :: TestTree +key_tests = testGroup "keys" [ + testCase "keypair derives the spec static keys" $ do + (i, r) <- spec_keypairs + want_i <- unhex ls_i_pub + want_r <- unhex ls_r_pub + BOLT8.serialize_pub (BOLT8.keypair_pub i) @?= want_i + BOLT8.serialize_pub (BOLT8.keypair_pub r) @?= want_r + , testCase "keypair rejects invalid secrets" $ do + n <- unhex + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" + n_1 <- unhex + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140" + assertBool "31 bytes" (isNothing (BOLT8.keypair (BS.replicate 31 1))) + assertBool "33 bytes" (isNothing (BOLT8.keypair (BS.replicate 33 1))) + assertBool "zero" (isNothing (BOLT8.keypair (BS.replicate 32 0))) + assertBool "order" (isNothing (BOLT8.keypair n)) + assertBool "2^256 - 1" (isNothing (BOLT8.keypair (BS.replicate 32 0xff))) + assertBool "order - 1" (isJust (BOLT8.keypair n_1)) + , testCase "parse_pub round-trips compressed keys" $ do + bs <- unhex ls_r_pub + pub <- expect_just "parse_pub" (BOLT8.parse_pub bs) + BOLT8.serialize_pub pub @?= bs + , testCase "parse_pub rejects other encodings" $ do + -- the generator, uncompressed and x-only + g_full <- unhex + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\ + \483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" + g_x <- unhex + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + bs <- unhex ls_r_pub + assertBool "uncompressed" (isNothing (BOLT8.parse_pub g_full)) + assertBool "x-only" (isNothing (BOLT8.parse_pub g_x)) + assertBool "0x04 prefix" + (isNothing (BOLT8.parse_pub (BS.cons 0x04 (BS.drop 1 bs)))) + assertBool "34 bytes" (isNothing (BOLT8.parse_pub (bs <> "\x00"))) + ] + +-- handshake ------------------------------------------------------------------ handshake_tests :: TestTree -handshake_tests = testGroup "Handshake" [ - testCase "act1 matches spec vector" test_act1 - , testCase "act2 matches spec vector" test_act2 - , testCase "act3 matches spec vector" test_act3 - , testCase "full handshake round-trip" test_full_handshake +handshake_tests = testGroup "handshake" [ + testCase "act one matches spec" $ do + (i, _) <- spec_keypairs + rs <- unhex ls_r_pub >>= expect_just "rs" . BOLT8.parse_pub + (msg1, _) <- expect_right "act1" (BOLT8.act1 i rs e_i_sec) + want <- unhex act1_msg + e <- unhex e_i_pub + msg1 @?= want + BS.take 33 (BS.drop 1 msg1) @?= e + , testCase "act two matches spec" $ do + (_, r) <- spec_keypairs + msg1 <- unhex act1_msg + (msg2, _) <- expect_right "act2" (BOLT8.act2 r e_r_sec msg1) + want <- unhex act2_msg + e <- unhex e_r_pub + msg2 @?= want + BS.take 33 (BS.drop 1 msg2) @?= e + , testCase "act three matches spec" $ do + (i, r) <- spec_keypairs + (_, i_hs) <- expect_right "act1" + (BOLT8.act1 i (BOLT8.keypair_pub r) e_i_sec) + msg2 <- unhex act2_msg + (msg3, _) <- expect_right "act3" (BOLT8.act3 i_hs msg2) + want <- unhex act3_msg + msg3 @?= want + , testCase "finalize accepts spec act three" $ do + (_, r) <- spec_keypairs + msg1 <- unhex act1_msg + msg3 <- unhex act3_msg + (_, r_hs) <- expect_right "act2" (BOLT8.act2 r e_r_sec msg1) + r_res <- expect_right "finalize" (BOLT8.finalize r_hs msg3) + want <- unhex ls_i_pub + BOLT8.serialize_pub (BOLT8.handshake_remote_static r_res) @?= want + , testCase "initiator records the responder's static key" $ do + (i_res, _) <- spec_results + want <- unhex ls_r_pub + BOLT8.serialize_pub (BOLT8.handshake_remote_static i_res) @?= want + , testCase "act1 rejects invalid entropy" $ do + (i, r) <- spec_keypairs + let rs = BOLT8.keypair_pub r + expect_error "zero" BOLT8.InvalidEntropy + (BOLT8.act1 i rs (BS.replicate 32 0)) + expect_error "short" BOLT8.InvalidEntropy + (BOLT8.act1 i rs (BS.replicate 31 0x12)) + , testCase "act2 rejects invalid entropy" $ do + (_, r) <- spec_keypairs + msg1 <- unhex act1_msg + expect_error "zero" BOLT8.InvalidEntropy + (BOLT8.act2 r (BS.replicate 32 0) msg1) + , testCase "acts reject oversized input" $ do + (i, r) <- spec_keypairs + msg1 <- unhex act1_msg + msg2 <- unhex act2_msg + msg3 <- unhex act3_msg + (_, i_hs) <- expect_right "act1" + (BOLT8.act1 i (BOLT8.keypair_pub r) e_i_sec) + (_, r_hs) <- expect_right "act2" (BOLT8.act2 r e_r_sec msg1) + expect_error "act2" BOLT8.InvalidLength + (BOLT8.act2 r e_r_sec (msg1 <> "\x00")) + expect_error "act3" BOLT8.InvalidLength + (BOLT8.act3 i_hs (msg2 <> "\x00")) + expect_error "finalize" BOLT8.InvalidLength + (BOLT8.finalize r_hs (msg3 <> "\x00")) ] -test_act1 :: Assertion -test_act1 = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (act1_msg, _) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - act1_msg @?= expected_act1 - -test_act2 :: Assertion -test_act2 = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, _) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, _) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - msg2 @?= expected_act2 - -test_act3 :: Assertion -test_act3 = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, _) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, _) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - msg3 @?= expected_act3 - -test_full_handshake :: Assertion -test_full_handshake = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - BOLT8.remote_static i_result @?= r_s_pub - BOLT8.remote_static r_result @?= i_s_pub - --- message encryption tests -------------------------------------------------- +-- Appendix A failure vectors ------------------------------------------------- -message_tests :: TestTree -message_tests = testGroup "Message Encryption" [ - testCase "message 0 matches spec" test_message_0 - , testCase "message 1 matches spec" test_message_1 - , testCase "message 500 matches spec" test_message_500 - , testCase "message 501 matches spec" test_message_501 - , testCase "message 1000 matches spec" test_message_1000 - , testCase "message 1001 matches spec" test_message_1001 - , testCase "decrypt round-trip" test_decrypt_roundtrip +data Failure = Failure String BS.ByteString BOLT8.Error + +handshake_failure_tests :: TestTree +handshake_failure_tests = testGroup "Appendix A failure vectors" [ + testGroup "initiator, act two" (fmap initiator_failure act2_failures) + , testGroup "responder, act one" (fmap act1_failure act1_failures) + , testGroup "responder, act three" (fmap act3_failure act3_failures) ] --- "hello" = 0x68656c6c6f -hello :: BS.ByteString -hello = "hello" - --- expected encrypted messages -expected_msg_0 :: BS.ByteString -expected_msg_0 = hex - "cf2b30ddf0cf3f80e7c35a6e6730b59fe802473180f396d88a8fb0db8cbcf25d\ - \2f214cf9ea1d95" - -expected_msg_1 :: BS.ByteString -expected_msg_1 = hex - "72887022101f0b6753e0c7de21657d35a4cb2a1f5cde2650528bbc8f837d0f0d\ - \7ad833b1a256a1" - -expected_msg_500 :: BS.ByteString -expected_msg_500 = hex - "178cb9d7387190fa34db9c2d50027d21793c9bc2d40b1e14dcf30ebeeeb220f4\ - \8364f7a4c68bf8" - -expected_msg_501 :: BS.ByteString -expected_msg_501 = hex - "1b186c57d44eb6de4c057c49940d79bb838a145cb528d6e8fd26dbe50a60ca2c\ - \104b56b60e45bd" - -expected_msg_1000 :: BS.ByteString -expected_msg_1000 = hex - "4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c05c5\ - \2b76b29b740f09" - -expected_msg_1001 :: BS.ByteString -expected_msg_1001 = hex - "2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e268338\ - \b1a16cf4ef2d36" - --- helper to get initiator session after handshake -get_initiator_session :: IO BOLT8.Session -get_initiator_session = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, _) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (_, result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - pure (BOLT8.session result) - --- encrypt N messages, return Nth ciphertext -encrypt_n :: Int -> BOLT8.Session -> IO BS.ByteString -encrypt_n n sess0 = go 0 sess0 - where - go i sess - | i == n = case BOLT8.encrypt sess hello of - Left err -> fail $ "encrypt failed at " ++ show i ++ ": " ++ show err - Right (ct, _) -> pure ct - | otherwise = case BOLT8.encrypt sess hello of - Left err -> fail $ "encrypt failed at " ++ show i ++ ": " ++ show err - Right (_, sess') -> go (i + 1) sess' - -test_message_0 :: Assertion -test_message_0 = do - sess <- get_initiator_session - ct <- encrypt_n 0 sess - ct @?= expected_msg_0 - -test_message_1 :: Assertion -test_message_1 = do - sess <- get_initiator_session - ct <- encrypt_n 1 sess - ct @?= expected_msg_1 - -test_message_500 :: Assertion -test_message_500 = do - sess <- get_initiator_session - ct <- encrypt_n 500 sess - ct @?= expected_msg_500 - -test_message_501 :: Assertion -test_message_501 = do - sess <- get_initiator_session - ct <- encrypt_n 501 sess - ct @?= expected_msg_501 - -test_message_1000 :: Assertion -test_message_1000 = do - sess <- get_initiator_session - ct <- encrypt_n 1000 sess - ct @?= expected_msg_1000 - -test_message_1001 :: Assertion -test_message_1001 = do - sess <- get_initiator_session - ct <- encrypt_n 1001 sess - ct @?= expected_msg_1001 - -test_decrypt_roundtrip :: Assertion -test_decrypt_roundtrip = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let i_sess = BOLT8.session i_result - r_sess = BOLT8.session r_result - (ct, _) <- expectRight "encrypt" (BOLT8.encrypt i_sess hello) - (pt, _) <- expectRight "decrypt" (BOLT8.decrypt r_sess ct) - pt @?= hello - --- framing tests ------------------------------------------------------------- - -framing_tests :: TestTree -framing_tests = testGroup "Packet Framing" [ - testCase "decrypt rejects trailing bytes" test_decrypt_trailing - , testCase "decrypt_frame returns remainder" test_decrypt_frame_remainder - , testCase "decrypt_frame handles multiple frames" test_decrypt_frame_multi +initiator_failure :: Failure -> TestTree +initiator_failure (Failure name input want) = testCase name $ do + (i, _) <- spec_keypairs + rs <- unhex ls_r_pub >>= expect_just "rs" . BOLT8.parse_pub + (msg1, i_hs) <- expect_right "act1" (BOLT8.act1 i rs e_i_sec) + unhex act1_msg >>= (msg1 @?=) + msg2 <- unhex input + expect_error name want (BOLT8.act3 i_hs msg2) + +act1_failure :: Failure -> TestTree +act1_failure (Failure name input want) = testCase name $ do + (_, r) <- spec_keypairs + msg1 <- unhex input + expect_error name want (BOLT8.act2 r e_r_sec msg1) + +act3_failure :: Failure -> TestTree +act3_failure (Failure name input want) = testCase name $ do + (_, r) <- spec_keypairs + msg1 <- unhex act1_msg + (msg2, r_hs) <- expect_right "act2" (BOLT8.act2 r e_r_sec msg1) + unhex act2_msg >>= (msg2 @?=) + msg3 <- unhex input + expect_error name want (BOLT8.finalize r_hs msg3) + +act2_failures :: [Failure] +act2_failures = [ + Failure "short read" + "0002466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27\ + \6e2470b93aac583c9ef6eafca3f730" + BOLT8.InvalidLength + , Failure "bad version" + "0102466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27\ + \6e2470b93aac583c9ef6eafca3f730ae" + BOLT8.InvalidVersion + , Failure "bad key serialization" + "0004466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27\ + \6e2470b93aac583c9ef6eafca3f730ae" + BOLT8.InvalidPub + , Failure "bad MAC" + "0002466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27\ + \6e2470b93aac583c9ef6eafca3f730af" + BOLT8.InvalidMAC ] -test_decrypt_trailing :: Assertion -test_decrypt_trailing = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let i_sess = BOLT8.session i_result - r_sess = BOLT8.session r_result - (ct, _) <- expectRight "encrypt" (BOLT8.encrypt i_sess hello) - -- append trailing bytes - let ct_with_trailing = ct <> "extra" - case BOLT8.decrypt r_sess ct_with_trailing of - Left BOLT8.InvalidLength -> pure () - Left err -> assertFailure $ "expected InvalidLength, got: " ++ show err - Right _ -> assertFailure "decrypt should reject trailing bytes" - -test_decrypt_frame_remainder :: Assertion -test_decrypt_frame_remainder = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let i_sess = BOLT8.session i_result - r_sess = BOLT8.session r_result - (ct, _) <- expectRight "encrypt" (BOLT8.encrypt i_sess hello) - let trailing = "remainder" - ct_with_trailing = ct <> trailing - (pt, remainder, _) <- expectRight "decrypt_frame" - (BOLT8.decrypt_frame r_sess ct_with_trailing) - pt @?= hello - remainder @?= trailing - -test_decrypt_frame_multi :: Assertion -test_decrypt_frame_multi = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let i_sess = BOLT8.session i_result - r_sess = BOLT8.session r_result - -- encrypt two messages - (ct1, i_sess') <- expectRight "encrypt 1" (BOLT8.encrypt i_sess "first") - (ct2, _) <- expectRight "encrypt 2" (BOLT8.encrypt i_sess' "second") - -- concatenate frames - let buffer = ct1 <> ct2 - -- decrypt first frame - (pt1, rest, r_sess') <- expectRight "frame 1" - (BOLT8.decrypt_frame r_sess buffer) - pt1 @?= "first" - -- decrypt second frame from remainder - (pt2, rest2, _) <- expectRight "frame 2" (BOLT8.decrypt_frame r_sess' rest) - pt2 @?= "second" - rest2 @?= BS.empty - --- partial framing tests ----------------------------------------------------- - -partial_framing_tests :: TestTree -partial_framing_tests = testGroup "Partial Framing" [ - testCase "short buffer returns NeedMore" test_partial_short_buffer - , testCase "partial body returns NeedMore" test_partial_body - , testCase "full frame returns FrameOk" test_partial_full_frame +act1_failures :: [Failure] +act1_failures = [ + Failure "short read" + "00036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f7\ + \0df6086551151f58b8afe6c195782c" + BOLT8.InvalidLength + , Failure "bad version" + "01036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f7\ + \0df6086551151f58b8afe6c195782c6a" + BOLT8.InvalidVersion + , Failure "bad key serialization" + "00046360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f7\ + \0df6086551151f58b8afe6c195782c6a" + BOLT8.InvalidPub + , Failure "bad MAC" + "00036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f7\ + \0df6086551151f58b8afe6c195782c6b" + BOLT8.InvalidMAC ] -test_partial_short_buffer :: Assertion -test_partial_short_buffer = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, _) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let r_sess = BOLT8.session r_result - short_buf = BS.replicate 10 0x00 - case BOLT8.decrypt_frame_partial r_sess short_buf of - BOLT8.NeedMore n -> n @?= 8 - BOLT8.FrameOk {} -> assertFailure "expected NeedMore, got FrameOk" - BOLT8.FrameError err -> - assertFailure $ "expected NeedMore, got: " ++ show err - -test_partial_body :: Assertion -test_partial_body = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let i_sess = BOLT8.session i_result - r_sess = BOLT8.session r_result - (ct, _) <- expectRight "encrypt" (BOLT8.encrypt i_sess hello) - -- take only length header (18 bytes) + 5 bytes of body - let partial = BS.take 23 ct - case BOLT8.decrypt_frame_partial r_sess partial of - BOLT8.NeedMore n -> do - -- "hello" = 5 bytes, so body = 5 + 16 = 21 - -- we have 5 bytes of body, need 16 more - n @?= 16 - BOLT8.FrameOk {} -> assertFailure "expected NeedMore, got FrameOk" - BOLT8.FrameError err -> - assertFailure $ "expected NeedMore, got: " ++ show err - -test_partial_full_frame :: Assertion -test_partial_full_frame = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, i_result) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let i_sess = BOLT8.session i_result - r_sess = BOLT8.session r_result - (ct, _) <- expectRight "encrypt" (BOLT8.encrypt i_sess hello) - let trailing = "extra" - buf = ct <> trailing - case BOLT8.decrypt_frame_partial r_sess buf of - BOLT8.FrameOk pt remainder _ -> do - pt @?= hello - remainder @?= trailing - BOLT8.NeedMore n -> - assertFailure $ "expected FrameOk, got NeedMore " ++ show n - BOLT8.FrameError err -> - assertFailure $ "expected FrameOk, got: " ++ show err - --- negative tests ------------------------------------------------------------ - -negative_tests :: TestTree -negative_tests = testGroup "Negative Tests" [ - testCase "act2 rejects wrong version" test_act2_wrong_version - , testCase "act2 rejects wrong length" test_act2_wrong_length - , testCase "act3 rejects invalid MAC" test_act3_invalid_mac - , testCase "finalize rejects invalid MAC" test_finalize_invalid_mac - , testCase "decrypt rejects short packet" test_decrypt_short_packet +act3_failures :: [Failure] +act3_failures = [ + Failure "bad version" + "01b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355\ + \361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139ba" + BOLT8.InvalidVersion + , Failure "short read" + "00b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355\ + \361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139" + BOLT8.InvalidLength + , Failure "bad MAC for ciphertext" + "00c9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355\ + \361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139ba" + BOLT8.InvalidMAC + , Failure "bad rs" + "00bfe3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355\ + \36ad09a8ee351870c2bb7f78b754a26c6cef79a98d25139c856d7efd252c2ae73c" + BOLT8.InvalidPub + , Failure "bad MAC" + "00b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355\ + \361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139bb" + BOLT8.InvalidMAC ] -test_act2_wrong_version :: Assertion -test_act2_wrong_version = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, _) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs initiator_e_priv) - let bad_msg1 = BS.cons 0x01 (BS.drop 1 msg1) - case BOLT8.act2 r_s_sec r_s_pub responder_e_priv bad_msg1 of - Left BOLT8.InvalidVersion -> pure () - Left err -> assertFailure $ "expected InvalidVersion, got: " ++ show err - Right _ -> assertFailure "expected rejection, got success" - -test_act2_wrong_length :: Assertion -test_act2_wrong_length = do - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - let short_msg = BS.replicate 49 0x00 - case BOLT8.act2 r_s_sec r_s_pub responder_e_priv short_msg of - Left BOLT8.InvalidLength -> pure () - Left err -> assertFailure $ "expected InvalidLength, got: " ++ show err - Right _ -> assertFailure "expected rejection, got success" - -test_act3_invalid_mac :: Assertion -test_act3_invalid_mac = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, _) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - bad_msg2 <- flip_byte 40 msg2 - case BOLT8.act3 i_hs bad_msg2 of - Left BOLT8.InvalidMAC -> pure () - Left err -> assertFailure $ "expected InvalidMAC, got: " ++ show err - Right _ -> assertFailure "expected rejection, got success" - -test_finalize_invalid_mac :: Assertion -test_finalize_invalid_mac = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, _) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - bad_msg3 <- flip_byte 20 msg3 - case BOLT8.finalize r_hs bad_msg3 of - Left BOLT8.InvalidMAC -> pure () - Left err -> assertFailure $ "expected InvalidMAC, got: " ++ show err - Right _ -> assertFailure "expected rejection, got success" - -test_decrypt_short_packet :: Assertion -test_decrypt_short_packet = do - (i_s_sec, i_s_pub) <- expectJust "initiator keypair" - (BOLT8.keypair initiator_s_priv) - (r_s_sec, r_s_pub) <- expectJust "responder keypair" - (BOLT8.keypair responder_s_priv) - rs <- expectJust "responder pub" (BOLT8.parse_pub responder_s_pub) - (msg1, i_hs) <- expectRight "act1" (BOLT8.act1 i_s_sec i_s_pub rs - initiator_e_priv) - (msg2, r_hs) <- expectRight "act2" (BOLT8.act2 r_s_sec r_s_pub responder_e_priv - msg1) - (msg3, _) <- expectRight "act3" (BOLT8.act3 i_hs msg2) - r_result <- expectRight "finalize" (BOLT8.finalize r_hs msg3) - let r_sess = BOLT8.session r_result - short_packet = BS.replicate 17 0x00 - case BOLT8.decrypt r_sess short_packet of - Left BOLT8.InvalidLength -> pure () - Left err -> assertFailure $ "expected InvalidLength, got: " ++ show err - Right _ -> assertFailure "expected rejection, got success" - --- flip one byte in a bytestring at given index -flip_byte :: Int -> BS.ByteString -> IO BS.ByteString -flip_byte i bs - | i < 0 || i >= BS.length bs = - assertFailure "flip_byte: index out of bounds" >> pure bs - | otherwise = - let (pre, post) = BS.splitAt i bs - b = BS.index post 0 - in pure (pre <> BS.cons (b `xor` 0xff) (BS.drop 1 post)) +-- message encryption --------------------------------------------------------- + +-- Appendix A message test: "hello" sent 1001 times by the initiator +spec_messages :: [(Int, BS.ByteString)] +spec_messages = [ + (0, "cf2b30ddf0cf3f80e7c35a6e6730b59fe802473180f396d88a8fb0db8cbc\ + \f25d2f214cf9ea1d95") + , (1, "72887022101f0b6753e0c7de21657d35a4cb2a1f5cde2650528bbc8f837d\ + \0f0d7ad833b1a256a1") + , (500, "178cb9d7387190fa34db9c2d50027d21793c9bc2d40b1e14dcf30ebeeeb2\ + \20f48364f7a4c68bf8") + , (501, "1b186c57d44eb6de4c057c49940d79bb838a145cb528d6e8fd26dbe50a60\ + \ca2c104b56b60e45bd") + , (1000, "4a2f3cc3b5e78ddb83dcb426d9863d9d9a723b0337c89dd0b005d89f8d3c\ + \05c52b76b29b740f09") + , (1001, "2ecd8c8a5629d0d02ab457a0fdd0f7b90a192cd46be5ecb6ca570bfc5e26\ + \8338b1a16cf4ef2d36") + ] + +-- the initiator's first n frames, each carrying "hello" +hello_stream :: Int -> IO [BS.ByteString] +hello_stream count = do + (i_res, _) <- spec_results + let go m s + | m == count = pure [] + | otherwise = do + (ct, s') <- expect_right "encrypt" (BOLT8.encrypt s "hello") + (ct :) <$> go (m + 1) s' + go (0 :: Int) (BOLT8.handshake_sender i_res) --- rotation tests ------------------------------------------------------------ +message_tests :: TestTree +message_tests = testGroup "messages" [ + testGroup "spec vectors" (fmap spec_message spec_messages) + , testCase "responder decrypts the spec stream" $ do + (_, r_res) <- spec_results + frames <- hello_stream 1002 + let go _ _ [] = pure () + go m r (f : fs) = do + (pt, r') <- expect_right ("decrypt " <> show m) (BOLT8.decrypt r f) + assertEqual ("message " <> show m) "hello" pt + go (m + 1) r' fs + go (0 :: Int) (BOLT8.handshake_receiver r_res) frames + , testCase "reference key rotation matches spec" $ do + ck <- unhex final_ck + sk <- unhex final_sk + ck1 <- unhex + "cc2c6e467efc8067720c2d09c139d1f77731893aad1defa14f9bf3c48d3f1d31" + k1 <- unhex + "3fbdc101abd1132ca3a0ae34a669d8d9ba69a587e0bb4ddd59524541cf4813d8" + ck2 <- unhex + "728366ed68565dc17cf6dd97330a859a6a56e87e2beef3bd828a4c4a54d8df06" + k2 <- unhex + "9e0477f9850dca41e42db0e4d154e3a098e5a000d995e421849fcd5df27882bd" + ref_rotate ck sk >>= (@?= (ck1, k1)) + ref_rotate ck1 k1 >>= (@?= (ck2, k2)) + , testCase "initiator sends under sk and ck" $ do + (i_res, _) <- spec_results + ck <- unhex final_ck + sk <- unhex final_sk + check_sender ck sk (BOLT8.handshake_sender i_res) + , testCase "initiator receives under rk and ck" $ do + (i_res, _) <- spec_results + ck <- unhex final_ck + rk <- unhex final_rk + check_receiver ck rk (BOLT8.handshake_receiver i_res) + , testCase "responder sends under rk and ck" $ do + (_, r_res) <- spec_results + ck <- unhex final_ck + rk <- unhex final_rk + check_sender ck rk (BOLT8.handshake_sender r_res) + , testCase "responder receives under sk and ck" $ do + (_, r_res) <- spec_results + ck <- unhex final_ck + sk <- unhex final_sk + check_receiver ck sk (BOLT8.handshake_receiver r_res) + , testCase "initiator to responder across rotations" $ do + (i_res, r_res) <- spec_results + round_trip 1600 + (BOLT8.handshake_sender i_res) (BOLT8.handshake_receiver r_res) + , testCase "responder to initiator across rotations" $ do + (i_res, r_res) <- spec_results + round_trip 1600 + (BOLT8.handshake_sender r_res) (BOLT8.handshake_receiver i_res) + ] + +spec_message :: (Int, BS.ByteString) -> TestTree +spec_message (m, h) = testCase ("message " <> show m) $ do + frames <- hello_stream 1002 + got <- expect_just "frame" (at m frames) + want <- unhex h + got @?= want + +-- transport ------------------------------------------------------------------ + +-- the spec handshake's transport states and the initiator's first two +-- frames, carrying "hello" and "world!" +transport_fixture + :: IO (BOLT8.Sender, BOLT8.Receiver, BOLT8.Receiver, BS.ByteString, + BS.ByteString) +transport_fixture = do + (i_res, r_res) <- spec_results + let i_snd = BOLT8.handshake_sender i_res + (f0, i_snd') <- expect_right "encrypt" (BOLT8.encrypt i_snd "hello") + (f1, _) <- expect_right "encrypt" (BOLT8.encrypt i_snd' "world!") + pure ( i_snd, BOLT8.handshake_receiver i_res + , BOLT8.handshake_receiver r_res, f0, f1 ) + +-- consume a buffer of frames via decrypt_header and decrypt_body +consume + :: BOLT8.Receiver + -> BS.ByteString + -> Either BOLT8.Error [BS.ByteString] +consume r buf + | BS.null buf = Right [] + | otherwise = do + let (hdr, rest) = BS.splitAt 18 buf + (n, p) <- BOLT8.decrypt_header r hdr + let (body, rest') = BS.splitAt n rest + (m, r') <- BOLT8.decrypt_body p body + (m :) <$> consume r' rest' + +transport_tests :: TestTree +transport_tests = testGroup "transport" [ + testCase "frame is message length plus 34 bytes" $ do + (_, _, _, f0, f1) <- transport_fixture + BS.length f0 @?= 5 + 34 + BS.length f1 @?= 6 + 34 + , testCase "decrypt_header reports the remaining length" $ do + (_, _, r, f0, _) <- transport_fixture + (n, _) <- expect_right "header" (BOLT8.decrypt_header r (BS.take 18 f0)) + n @?= 5 + 16 + , testCase "header and body decrypt consecutive frames" $ do + (_, _, r, f0, f1) <- transport_fixture + ms <- expect_right "consume" (consume r (f0 <> f1)) + ms @?= ["hello", "world!"] + , testCase "decrypt agrees with header and body" $ do + (_, _, r, f0, f1) <- transport_fixture + (m0, r') <- expect_right "decrypt 0" (BOLT8.decrypt r f0) + (m1, _) <- expect_right "decrypt 1" (BOLT8.decrypt r' f1) + [m0, m1] @?= ["hello", "world!"] + , testCase "empty message round-trips" $ do + (s, _, r, _, _) <- transport_fixture + (f, _) <- expect_right "encrypt" (BOLT8.encrypt s mempty) + BS.length f @?= 34 + (m, _) <- expect_right "decrypt" (BOLT8.decrypt r f) + m @?= mempty + , testCase "65535-byte message round-trips" $ do + (s, _, r, _, _) <- transport_fixture + let big = BS.replicate 65535 0xab + (f, _) <- expect_right "encrypt" (BOLT8.encrypt s big) + BS.length f @?= 65569 + (n, _) <- expect_right "header" (BOLT8.decrypt_header r (BS.take 18 f)) + n @?= 65551 + (m, _) <- expect_right "decrypt" (BOLT8.decrypt r f) + m @?= big + , testCase "encrypt rejects a 65536-byte message" $ do + (s, _, _, _, _) <- transport_fixture + expect_error "encrypt" BOLT8.InvalidLength + (BOLT8.encrypt s (BS.replicate 65536 0)) + ] -rotation_tests :: TestTree -rotation_tests = testGroup "Key Rotation" [ - testCase "send key rotates when nonce is past 1000" - test_rotate_past_1000_send - , testCase "receive key rotates when nonce is past 1000" - test_rotate_past_1000_recv +transport_failure_tests :: TestTree +transport_failure_tests = testGroup "transport failures" [ + testCase "bad length MAC" $ do + (_, _, r, f0, _) <- transport_fixture + for_bytes [2, 17] f0 $ \bad -> do + expect_error "decrypt" BOLT8.InvalidMAC (BOLT8.decrypt r bad) + expect_error "decrypt_header" BOLT8.InvalidMAC + (BOLT8.decrypt_header r (BS.take 18 bad)) + , testCase "tampered length" $ do + (_, _, r, f0, _) <- transport_fixture + for_bytes [0, 1] f0 $ \bad -> do + expect_error "decrypt" BOLT8.InvalidMAC (BOLT8.decrypt r bad) + expect_error "decrypt_header" BOLT8.InvalidMAC + (BOLT8.decrypt_header r (BS.take 18 bad)) + , testCase "bad body MAC" $ do + (_, _, r, f0, _) <- transport_fixture + for_bytes [18, 23, BS.length f0 - 1] f0 $ \bad -> do + expect_error "decrypt" BOLT8.InvalidMAC (BOLT8.decrypt r bad) + (n, p) <- expect_right "header" + (BOLT8.decrypt_header r (BS.take 18 bad)) + expect_error "decrypt_body" BOLT8.InvalidMAC + (BOLT8.decrypt_body p (BS.take n (BS.drop 18 bad))) + , testCase "truncated frame" $ do + (_, _, r, f0, _) <- transport_fixture + let short = BS.take (BS.length f0 - 1) f0 + expect_error "decrypt" BOLT8.InvalidLength (BOLT8.decrypt r short) + expect_error "decrypt, header only" BOLT8.InvalidLength + (BOLT8.decrypt r (BS.take 18 f0)) + expect_error "decrypt, partial header" BOLT8.InvalidLength + (BOLT8.decrypt r (BS.take 17 f0)) + expect_error "decrypt, empty" BOLT8.InvalidLength + (BOLT8.decrypt r mempty) + expect_error "decrypt_header" BOLT8.InvalidLength + (BOLT8.decrypt_header r (BS.take 17 f0)) + (_, p) <- expect_right "header" (BOLT8.decrypt_header r (BS.take 18 f0)) + expect_error "decrypt_body" BOLT8.InvalidLength + (BOLT8.decrypt_body p (BS.drop 18 short)) + , testCase "trailing bytes" $ do + (_, _, r, f0, _) <- transport_fixture + let long = f0 <> "\x00" + expect_error "decrypt" BOLT8.InvalidLength (BOLT8.decrypt r long) + expect_error "decrypt_header" BOLT8.InvalidLength + (BOLT8.decrypt_header r (BS.take 19 f0)) + (_, p) <- expect_right "header" (BOLT8.decrypt_header r (BS.take 18 f0)) + expect_error "decrypt_body" BOLT8.InvalidLength + (BOLT8.decrypt_body p (BS.drop 18 long)) + , testCase "replayed frame" $ do + (_, _, r, f0, _) <- transport_fixture + (_, r') <- expect_right "decrypt" (BOLT8.decrypt r f0) + expect_error "replay" BOLT8.InvalidMAC (BOLT8.decrypt r' f0) + , testCase "reordered frame" $ do + (_, _, r, _, f1) <- transport_fixture + expect_error "decrypt" BOLT8.InvalidMAC (BOLT8.decrypt r f1) + , testCase "reflected frame" $ do + (_, i_rcv, _, f0, _) <- transport_fixture + expect_error "decrypt" BOLT8.InvalidMAC (BOLT8.decrypt i_rcv f0) ] + where + for_bytes is f act = mapM_ (\i -> flip_byte i f >>= act) is --- A session whose nonces are already past 1000 must still rotate on --- the next step rather than letting the nonce grow without bound. -test_rotate_past_1000_send :: Assertion -test_rotate_past_1000_send = do - sess <- get_initiator_session - let stale = sess { I.sess_sn = I.SessionNonce 1001 } - (_, sess') <- expectRight "encrypt" (BOLT8.encrypt stale hello) - I.unSessionNonce (I.sess_sn sess') @?= 1 - assertBool "send key rotated" (I.sess_sk sess' /= I.sess_sk sess) - assertBool "send ck rotated" (I.sess_sck sess' /= I.sess_sck sess) - -test_rotate_past_1000_recv :: Assertion -test_rotate_past_1000_recv = do - i_sess <- get_initiator_session - let i_stale = i_sess { I.sess_sn = I.SessionNonce 1001 } - r_stale = I.Session { - I.sess_sk = I.sess_rk i_sess - , I.sess_sn = I.SessionNonce 0 - , I.sess_sck = I.sess_rck i_sess - , I.sess_rk = I.sess_sk i_sess - , I.sess_rn = I.SessionNonce 1001 - , I.sess_rck = I.sess_sck i_sess - } - (ct, _) <- expectRight "encrypt" (BOLT8.encrypt i_stale hello) - (pt, r_sess') <- expectRight "decrypt" (BOLT8.decrypt r_stale ct) - pt @?= hello - I.unSessionNonce (I.sess_rn r_sess') @?= 1 - assertBool "recv key rotated" (I.sess_rk r_sess' /= I.sess_rk r_stale) - --- utilities ----------------------------------------------------------------- - --- Safe hex decode for test vectors (only called at top level with known-good --- literals). This uses error since it's for compile-time constants, not runtime --- input; wrapping in IO would break the test vector declarations. -hex :: BS.ByteString -> BS.ByteString -hex bs = case B16.decode bs of - Nothing -> error "hex: invalid test vector literal" - Just r -> r - --- property tests -------------------------------------------------------------- +-- properties ----------------------------------------------------------------- property_tests :: TestTree -property_tests = testGroup "Properties" [ - testProperty "handshake round-trip" prop_handshake_roundtrip - , testProperty "encrypt/decrypt round-trip" - prop_encrypt_decrypt_roundtrip - , testProperty "decrypt_frame consumes one frame" - prop_frame_consumes_one - , testProperty "decrypt_frame_partial NeedMore on short" - prop_partial_needmore_short - , testProperty "handshake authenticates remote statics" - prop_handshake_remote_statics - , testProperty "encrypt/decrypt survives key rotation" - prop_key_rotation_roundtrip - , testProperty "mkMessagePayload validates size" - prop_mkMessagePayload_validates - , testProperty "key32 validates length" - prop_key32_validates +property_tests = testGroup "properties" [ + Q.testProperty "handshake agrees on keys and statics" prop_handshake + , Q.testProperty "messages up to 65535 bytes round-trip" prop_size + , Q.testProperty "header and body recover a stream" prop_stream + , Q.testProperty "parse_pub inverts serialize_pub" prop_pub ] --- generators ------------------------------------------------------------------ - --- | Generate 32 bytes of entropy that yields a valid keypair. -genValidEntropy :: Gen BS.ByteString -genValidEntropy = do - bytes <- BS.pack <$> vectorOf 32 (choose (0, 255)) - case BOLT8.keypair bytes of - Just _ -> pure bytes - Nothing -> genValidEntropy - --- | Generate a payload of 0..256 bytes. -genPayload :: Gen BS.ByteString -genPayload = do - len <- choose (0, 256) - BS.pack <$> vectorOf len (choose (0, 255)) - --- | Perform a full handshake with given static key entropy. --- Uses fixed ephemeral keys for determinism. -doHandshake - :: BS.ByteString - -> BS.ByteString - -> Maybe (BOLT8.Session, BOLT8.Session) -doHandshake i_entropy r_entropy = do - (i_res, r_res) <- doHandshake' i_entropy r_entropy - pure (BOLT8.session i_res, BOLT8.session r_res) - --- | Like 'doHandshake' but returns full Handshake results. -doHandshake' - :: BS.ByteString - -> BS.ByteString - -> Maybe (BOLT8.Handshake, BOLT8.Handshake) -doHandshake' i_entropy r_entropy = do - (i_s_sec, i_s_pub) <- BOLT8.keypair i_entropy - (r_s_sec, r_s_pub) <- BOLT8.keypair r_entropy - let i_e = BS.replicate 32 0x12 - r_e = BS.replicate 32 0x22 - (msg1, i_hs) <- either (const Nothing) Just $ - BOLT8.act1 i_s_sec i_s_pub r_s_pub i_e - (msg2, r_hs) <- either (const Nothing) Just $ - BOLT8.act2 r_s_sec r_s_pub r_e msg1 - (msg3, i_res) <- either (const Nothing) Just $ - BOLT8.act3 i_hs msg2 - r_res <- either (const Nothing) Just $ - BOLT8.finalize r_hs msg3 - pure (i_res, r_res) - --- | Send n messages from initiator to responder, --- advancing both session states in sync. -advanceSessions - :: Int - -> BOLT8.Session - -> BOLT8.Session - -> Maybe (BOLT8.Session, BOLT8.Session) -advanceSessions 0 i r = Just (i, r) -advanceSessions n i r = - case BOLT8.encrypt i (BS.replicate 5 0x00) of - Left _ -> Nothing - Right (ct, i') -> - case BOLT8.decrypt r ct of - Left _ -> Nothing - Right (_, r') -> advanceSessions (n - 1) i' r' - --- properties ------------------------------------------------------------------ - --- | Handshake succeeds for valid keys and sessions are consistent. -prop_handshake_roundtrip :: Property -prop_handshake_roundtrip = forAll genValidEntropy $ \i_ent -> - forAll genValidEntropy $ \r_ent -> - case doHandshake i_ent r_ent of - Nothing -> False - Just _ -> True - --- | Encrypt then decrypt yields original payload. -prop_encrypt_decrypt_roundtrip :: Property -prop_encrypt_decrypt_roundtrip = forAll genPayload $ \payload -> - case doHandshake initiator_s_priv responder_s_priv of - Nothing -> False - Just (i_sess, r_sess) -> - case BOLT8.encrypt i_sess payload of - Left _ -> False - Right (ct, _) -> - case BOLT8.decrypt r_sess ct of - Left _ -> False - Right (pt, _) -> pt == payload - --- | decrypt_frame consumes exactly one frame and returns remainder. -prop_frame_consumes_one :: Property -prop_frame_consumes_one = forAll genPayload $ \p1 -> - forAll genPayload $ \p2 -> - case doHandshake initiator_s_priv responder_s_priv of - Nothing -> False - Just (i_sess, r_sess) -> - case BOLT8.encrypt i_sess p1 of - Left _ -> False - Right (ct1, i_sess') -> - case BOLT8.encrypt i_sess' p2 of - Left _ -> False - Right (ct2, _) -> - let buf = ct1 <> ct2 - in case BOLT8.decrypt_frame r_sess buf of - Left _ -> False - Right (pt1, rest, r_sess') -> - pt1 == p1 && - case BOLT8.decrypt_frame r_sess' rest of - Left _ -> False - Right (pt2, rest2, _) -> - pt2 == p2 && BS.null rest2 - --- | decrypt_frame_partial returns NeedMore when buffer < 18 bytes. -prop_partial_needmore_short :: Property -prop_partial_needmore_short = forAll (choose (0, 17)) $ \len -> - case doHandshake initiator_s_priv responder_s_priv of - Nothing -> False - Just (_, r_sess) -> - let buf = BS.replicate len 0x00 - in case BOLT8.decrypt_frame_partial r_sess buf of - BOLT8.NeedMore n -> n == 18 - len - _ -> False - --- | Handshake authenticates remote static keys: each side --- sees the other's static pubkey. -prop_handshake_remote_statics :: Property -prop_handshake_remote_statics = - forAll genValidEntropy $ \i_ent -> - forAll genValidEntropy $ \r_ent -> - case (doHandshake' i_ent r_ent, - BOLT8.keypair i_ent, - BOLT8.keypair r_ent) of - (Just (i_res, r_res), - Just (_, i_pub), - Just (_, r_pub)) -> - BOLT8.remote_static i_res == r_pub - && BOLT8.remote_static r_res == i_pub - _ -> False - --- | Encrypt/decrypt roundtrip survives key rotation at --- nonce 1000. Each encrypt uses 2 nonces (length + --- body), so rotation happens after 500 messages. --- We advance to message 499 then send a test payload --- across the rotation boundary. -prop_key_rotation_roundtrip :: Property -prop_key_rotation_roundtrip = forAll genPayload $ \payload -> - case doHandshake initiator_s_priv responder_s_priv of - Nothing -> False - Just (i_sess, r_sess) -> - case advanceSessions 499 i_sess r_sess of - Nothing -> False - Just (i_sess', r_sess') -> - case BOLT8.encrypt i_sess' payload of - Left _ -> False - Right (ct, _) -> - case BOLT8.decrypt r_sess' ct of - Left _ -> False - Right (pt, _) -> pt == payload - --- | mkMessagePayload accepts payloads <= 65535 bytes and --- rejects payloads > 65535. -prop_mkMessagePayload_validates :: Property -prop_mkMessagePayload_validates = - forAll (choose (0, 256)) $ \len -> - let bs = BS.replicate len 0x00 - in case BOLT8.mkMessagePayload bs of - Right mp -> - BOLT8.unMessagePayload mp === bs - Left _ -> False === True - --- | key32 accepts exactly 32-byte inputs and rejects others. -prop_key32_validates :: Property -prop_key32_validates = - forAll (choose (0, 64)) $ \len -> - let bs = BS.replicate len 0x00 - in case BOLT8.key32 bs of - Just k -> len === 32 - .&&. BOLT8.unKey32 k === bs - Nothing -> (len /= 32) === True +-- 32 bytes that form a valid secret key +gen_secret :: Q.Gen BS.ByteString +gen_secret = (BS.pack <$> Q.vectorOf 32 Q.arbitrary) + `Q.suchThat` (isJust . BOLT8.keypair) + +gen_bytes :: Int -> Q.Gen BS.ByteString +gen_bytes n = do + len <- Q.choose (0, n) + BS.pack <$> Q.vectorOf len Q.arbitrary + +prop_handshake :: Q.Property +prop_handshake = + Q.forAll gen_secret $ \i_s -> + Q.forAll gen_secret $ \r_s -> + Q.forAll gen_secret $ \i_e -> + Q.forAll gen_secret $ \r_e -> + Q.forAll (gen_bytes 256) $ \m -> + case (BOLT8.keypair i_s, BOLT8.keypair r_s) of + (Just i, Just r) -> check i r (handshake i r i_e r_e) m + _ -> Q.counterexample "keypair" False + where + check _ _ (Left e) _ = Q.counterexample (show e) False + check i r (Right (i_res, r_res)) m = + let there = send (BOLT8.handshake_sender i_res) + (BOLT8.handshake_receiver r_res) m + back = send (BOLT8.handshake_sender r_res) + (BOLT8.handshake_receiver i_res) m + in BOLT8.handshake_remote_static i_res + Q.=== BOLT8.keypair_pub r + Q..&&. BOLT8.handshake_remote_static r_res + Q.=== BOLT8.keypair_pub i + Q..&&. there Q.=== Right m + Q..&&. back Q.=== Right m + send s r m = do + (ct, _) <- BOLT8.encrypt s m + fst <$> BOLT8.decrypt r ct + +prop_size :: Q.Property +prop_size = + Q.forAll gen_len $ \len -> + Q.forAll Q.arbitrary $ \b -> + let m = BS.replicate len b + in case spec_handshake of + Left e -> Q.counterexample (show e) False + Right (i_res, r_res) -> + case BOLT8.encrypt (BOLT8.handshake_sender i_res) m of + Left e -> Q.counterexample (show e) False + Right (f, _) -> + let r = BOLT8.handshake_receiver r_res + in BS.length f Q.=== len + 34 + Q..&&. fmap fst (BOLT8.decrypt_header r (BS.take 18 f)) + Q.=== Right (len + 16) + Q..&&. fmap fst (BOLT8.decrypt r f) Q.=== Right m + where + gen_len = Q.frequency [ + (1, pure 0) + , (1, pure 65535) + , (8, Q.choose (0, 65535)) + ] + +prop_stream :: Q.Property +prop_stream = + Q.forAll (Q.listOf (gen_bytes 512)) $ \ms -> + case spec_handshake of + Left e -> Q.counterexample (show e) False + Right (i_res, r_res) -> + let enc _ [] = Right [] + enc s (m : rest) = do + (f, s') <- BOLT8.encrypt s m + (f :) <$> enc s' rest + in case enc (BOLT8.handshake_sender i_res) ms of + Left e -> Q.counterexample (show e) False + Right fs -> + consume (BOLT8.handshake_receiver r_res) (BS.concat fs) + Q.=== Right ms + +prop_pub :: Q.Property +prop_pub = Q.forAll gen_secret $ \sec -> + case BOLT8.keypair sec of + Nothing -> Q.counterexample "keypair" False + Just kp -> + let pub = BOLT8.keypair_pub kp + in BOLT8.parse_pub (BOLT8.serialize_pub pub) Q.=== Just pub