commit f2dfffdce6dd1d0fb3135cf967ee23a466f26fac
parent 507af14c14be9fbaa27992357aa25e40c443daee
Author: Jared Tobin <jared@jtobin.io>
Date: Sat, 10 Oct 2026 13:02:14 +0400
Merge fix/tlv-length: TLV length wrap, point validation
- decodeTlvStreamRaw and decodeTlvStreamWith compared a Word64 TLV
length against the remaining input after converting it to Int, so
lengths >= 2^63 wrapped negative and were accepted with an empty
value. bolt2 and bolt7 inherit the fix through decodeTlvStreamRaw.
- point now rejects inputs without a 0x02/0x03 prefix, per the BOLT #1
Appendix B invalid-point vector.
- Regression tests for both.
Diffstat:
3 files changed, 35 insertions(+), 7 deletions(-)
diff --git a/lib/Lightning/Protocol/BOLT1/Prim.hs b/lib/Lightning/Protocol/BOLT1/Prim.hs
@@ -194,11 +194,15 @@ instance NFData Point
-- | Construct a 'Point' from a 33-byte 'BS.ByteString'.
--
--- Returns 'Nothing' if the input is not exactly 33 bytes.
+-- Returns 'Nothing' if the input is not exactly 33 bytes, or does not
+-- begin with a compressed-encoding prefix (0x02 or 0x03). The curve
+-- equation is not checked.
point :: BS.ByteString -> Maybe Point
point !bs
- | BS.length bs == 33 = Just (Point bs)
- | otherwise = Nothing
+ | BS.length bs == 33
+ , let !h = BS.index bs 0
+ , h == 0x02 || h == 0x03 = Just (Point bs)
+ | otherwise = Nothing
{-# INLINE point #-}
-- | Extract the raw bytes from a 'Point'.
diff --git a/lib/Lightning/Protocol/BOLT1/TLV.hs b/lib/Lightning/Protocol/BOLT1/TLV.hs
@@ -136,8 +136,8 @@ decodeTlvStreamRaw = go Nothing []
Nothing -> pure ()
(len, rest2) <- maybe (Left TlvNonMinimalEncoding) Right
(decodeBigSize rest1)
- -- Length bounds check
- when (fromIntegral len > BS.length rest2) $
+ -- Length bounds check (in Word64, so huge lengths can't wrap)
+ when (len > fromIntegral (BS.length rest2)) $
Left TlvLengthExceedsBounds
let !val = BS.take (fromIntegral len) rest2
!rest3 = BS.drop (fromIntegral len) rest2
@@ -172,8 +172,8 @@ decodeTlvStreamWith isKnown = go Nothing []
Nothing -> pure ()
(len, rest2) <- maybe (Left TlvNonMinimalEncoding) Right
(decodeBigSize rest1)
- -- Length bounds check
- when (fromIntegral len > BS.length rest2) $
+ -- Length bounds check (in Word64, so huge lengths can't wrap)
+ when (len > fromIntegral (BS.length rest2)) $
Left TlvLengthExceedsBounds
let !val = BS.take (fromIntegral len) rest2
!rest3 = BS.drop (fromIntegral len) rest2
diff --git a/test/Main.hs b/test/Main.hs
@@ -96,6 +96,18 @@ primitive_tests = testGroup "Primitives" [
decodeU32 (BS.pack [0x01, 0x02]) @?= Nothing
, testCase "decodeU64 insufficient" $
decodeU64 (BS.pack [0x01, 0x02, 0x03, 0x04]) @?= Nothing
+ , testCase "point accepts 0x02 prefix" $
+ fmap unPoint (point (BS.cons 0x02 (BS.replicate 32 0x01)))
+ @?= Just (BS.cons 0x02 (BS.replicate 32 0x01))
+ , testCase "point accepts 0x03 prefix" $
+ fmap unPoint (point (BS.cons 0x03 (BS.replicate 32 0x01)))
+ @?= Just (BS.cons 0x03 (BS.replicate 32 0x01))
+ , testCase "point rejects 0x04 prefix (Appendix B)" $
+ point (unhex ("043da092f6980e58d2c037173180e9a465476026ee50f9"
+ <> "6695963e8efe436f54eb"))
+ @?= Nothing
+ , testCase "point rejects wrong length" $
+ point (BS.cons 0x02 (BS.replicate 31 0x01)) @?= Nothing
]
-- Signed integer tests ---------------------------------------------------------
@@ -281,6 +293,18 @@ tlv_tests = testGroup "TLV" [
]
, testCase "empty stream" $
decodeTlvStream "" @?= Right (unsafeTlvStream [])
+ , testCase "length 2^64-1 rejected (raw)" $
+ decodeTlvStreamRaw (unhex "01ffffffffffffffffff")
+ @?= Left TlvLengthExceedsBounds
+ , testCase "length 2^63 rejected (raw)" $
+ decodeTlvStreamRaw (unhex "01ff800000000000000067")
+ @?= Left TlvLengthExceedsBounds
+ , testCase "length 2^64-1 rejected (with predicate)" $
+ decodeTlvStreamWith (const True) (unhex "01ffffffffffffffffff")
+ @?= Left TlvLengthExceedsBounds
+ , testCase "huge length not reparsed as later records" $
+ decodeTlvStreamRaw (unhex "01ffffffffffffffffff6701aa")
+ @?= Left TlvLengthExceedsBounds
, testCase "single record type 1" $ do
let bs = mconcat [
encodeBigSize 1 -- type