ppad-fixed 0.1.6 → 0.2.2
raw patch · 11 files changed
Files
- CHANGELOG +30/−0
- lib/Data/Choice.hs +9/−0
- lib/Data/Word/Limb.hs +9/−1
- lib/Data/Word/Wide.hs +1/−1
- lib/Data/Word/Wider.hs +2/−2
- lib/Numeric/Montgomery/Secp256k1/Curve.hs +14/−9
- lib/Numeric/Montgomery/Secp256k1/Scalar.hs +14/−9
- ppad-fixed.cabal +6/−1
- test/Montgomery/Curve.hs +29/−1
- test/Montgomery/Scalar.hs +34/−1
- test/Wider.hs +21/−1
CHANGELOG view
@@ -1,5 +1,35 @@ # Changelog +- 0.2.2 (2026-11-07)+ * Fixes 'odd#' and 'odd_vartime' in both Montgomery domains, which+ took the parity of a value's Montgomery form rather than of the+ value itself, and so returned the wrong answer about half the time.++ * Fixes 'fromInteger' in both Montgomery domains, which reduced its+ argument modulo 2 ^ 256 before reducing it modulo the domain's+ modulus, and so mapped negative integers, and those of 2 ^ 256 or+ more, to the wrong values.++- 0.2.1 (2026-10-27)+ * Under the llvm flag on x86_64, the library is compiled with LLVM's+ x86 cmov converter off (-optlc=-x86-cmov-converter=false). The+ converter rewrites a conditional move whose operand is a load as a+ branch, and so made the borrow-masked correction of sub_mod,+ sub_mod#, and sub_mod_c# a branch on the borrow, which depends on+ the operands. NCG builds, and aarch64, were unaffected.++- 0.2.0 (2026-08-01)+ * Addresses a couple of minor issues detected by an LLM scan:++ * Fixes a bug that could lead shl1_c# to produce a malformed Choice+ that would behave incorrectly for negation or selection.++ * Each Montgomery domain's 'signum' implementation was broken,+ producing R^-1 instead of the proper in-domain 'one'. Now these+ return the appropriate values.++ Also clarifies a few Haddocks, on the suggestions of the same scan.+ - 0.1.6 (2026-08-01) * The eq_{word,wide,wider}# functions now return a mask, instead of a bit, inside the resulting Choice value.
lib/Data/Choice.hs view
@@ -305,6 +305,9 @@ -- | Select an unboxed word without branching, given a 'Choice'. --+-- Returns the second word if the 'Choice' is truthy, and the first+-- otherwise.+-- -- >>> let w = C.select_word# 0## 1## (C.true# ()) in GHC.Word.W# w -- 1 select_word# :: Word# -> Word# -> Choice -> Word#@@ -312,6 +315,9 @@ {-# INLINE select_word# #-} -- | Select an unboxed two-limb word without branching, given a 'Choice'.+--+-- Returns the second word if the 'Choice' is truthy, and the first+-- otherwise. select_wide# :: Limb2 -> Limb2@@ -323,6 +329,9 @@ {-# INLINE select_wide# #-} -- | Select an unboxed four-limb word without branching, given a 'Choice'.+--+-- Returns the second word if the 'Choice' is truthy, and the first+-- otherwise. select_wider# :: Limb4 -> Limb4
lib/Data/Word/Limb.hs view
@@ -143,7 +143,7 @@ -- constant-time selection ---------------------------------------------------- --- | Return a if c is truthy, otherwise return b.+-- | Return b if c is truthy, otherwise return a. select# :: Limb -- ^ a -> Limb -- ^ b@@ -268,6 +268,9 @@ -- | Carrying addition, computing augend + addend + carry, returning -- the sum and new carry.+--+-- The carry-in must be 0 or 1; this is not checked. The carry-out+-- is always 0 or 1. add_c# :: Limb -- ^ augend -> Limb -- ^ addend@@ -304,6 +307,11 @@ -- | Borrowing subtraction, computing minuend - (subtrahend + borrow), -- returning the difference and new borrow mask.+--+-- The borrow-in is interpreted as a mask, of which only the most+-- significant bit is examined; a full-word mask (as produced by+-- this function) borrows 1. The borrow-out is 0 or a full-word+-- mask. sub_b# :: Limb -- ^ minuend -> Limb -- ^ subtrahend
lib/Data/Word/Wide.hs view
@@ -159,7 +159,7 @@ -- constant-time selection----------------------------------------------------- --- | Return a if c is truthy, otherwise return b.+-- | Return b if c is truthy, otherwise return a. -- -- >>> import qualified Data.Choice as C -- >>> select 0 1 (C.true# ())
lib/Data/Word/Wider.hs view
@@ -319,7 +319,7 @@ in L4 w0 w1 w2 w3 {-# INLINE select# #-} --- | Return a if c is truthy, otherwise return b.+-- | Return b if c is truthy, otherwise return a. -- -- >>> import qualified Data.Choice as C -- >>> select 0 1 (C.true# ())@@ -384,7 +384,7 @@ !r2 = L.or# s2 c1 !(# s3, c3 #) = (# L.shl# w3 1#, L.shr# w3 s #) !r3 = L.or# s3 c2- !(Limb w) = L.shl# c3 s+ !(Limb w) = c3 in (# (# r0, r1, r2, r3 #), C.from_bit# w #) {-# INLINE shl1_c# #-}
lib/Numeric/Montgomery/Secp256k1/Curve.hs view
@@ -105,12 +105,14 @@ a * b = mul a b negate a = neg a abs = id- fromInteger = to . WW.to_vartime+ fromInteger n = to (WW.to_vartime (n `mod` p)) where+ p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F signum (Montgomery (# l0, l1, l2, l3 #)) = let !(Limb l) = l0 `L.or#` l1 `L.or#` l2 `L.or#` l3 !n = C.from_word_nonzero# l- !b = C.to_word# n- in Montgomery (# Limb b, Limb 0##, Limb 0##, Limb 0## #)+ !(Montgomery z) = zero+ !(Montgomery o) = one+ in Montgomery (select# z o n) instance NFData Montgomery where rnf (Montgomery a) = case a of (# _, _, _, _ #) -> ()@@ -761,6 +763,9 @@ -- | Multiplicative inverse in the Montgomery domain. --+-- Note that 'zero' has no multiplicative inverse; 'inv' returns+-- 'zero' when applied to it.+-- -- >> inv 2 -- 57896044618658097711785492504343953926634992332820282019728792003954417335832 -- >> inv 2 * 2@@ -1326,8 +1331,10 @@ in loop o b e (256 :: Word) {-# INLINE exp# #-} +-- parity isn't preserved by the montgomery form, so it's taken of the+-- retrieved value odd# :: Limb4 -> C.Choice-odd# = WW.odd#+odd# m = WW.odd# (retr# m) {-# INLINE odd# #-} -- | Check if a 'Montgomery' value is odd.@@ -1335,12 +1342,10 @@ -- Note that the comparison is performed in constant time, but we -- branch when converting to 'Bool'. ----- >>> odd 1+-- >>> odd_vartime 1 -- True--- >>> odd 2+-- >>> odd_vartime 2 -- False--- >>> Data.Word.Wider.odd (retr 3) -- parity is preserved--- True odd_vartime :: Montgomery -> Bool odd_vartime (Montgomery m) = C.decide (odd# m) @@ -1354,7 +1359,7 @@ select# = WW.select# {-# INLINE select# #-} --- | Return a if c is truthy, otherwise return b.+-- | Return b if c is truthy, otherwise return a. -- -- >>> import qualified Data.Choice as C -- >>> select 0 1 (C.true# ())
lib/Numeric/Montgomery/Secp256k1/Scalar.hs view
@@ -103,12 +103,14 @@ a * b = mul a b negate a = neg a abs = id- fromInteger = to . WW.to_vartime+ fromInteger n = to (WW.to_vartime (n `mod` q)) where+ q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 signum (Montgomery (# l0, l1, l2, l3 #)) = let !(Limb l) = l0 `L.or#` l1 `L.or#` l2 `L.or#` l3 !n = C.from_word_nonzero# l- !b = C.to_word# n- in Montgomery (L4 b 0## 0## 0##)+ !(Montgomery z) = zero+ !(Montgomery o) = one+ in Montgomery (select# z o n) instance NFData Montgomery where rnf (Montgomery a) = case a of (# _, _, _, _ #) -> ()@@ -792,6 +794,9 @@ -- | Multiplicative inverse in the Montgomery domain. --+-- Note that 'zero' has no multiplicative inverse; 'inv' returns+-- 'zero' when applied to it.+-- -- >> inv 2 -- 57896044618658097711785492504343953926418782139537452191302581570759080747169 -- >> inv 2 * 2@@ -828,8 +833,10 @@ in loop o b e (256 :: Word) {-# INLINE exp# #-} +-- parity isn't preserved by the montgomery form, so it's taken of the+-- retrieved value odd# :: Limb4 -> C.Choice-odd# = WW.odd#+odd# m = WW.odd# (retr# m) {-# INLINE odd# #-} -- | Check if a 'Montgomery' value is odd.@@ -837,12 +844,10 @@ -- Note that the comparison is performed in constant time, but we -- branch when converting to 'Bool'. ----- >>> odd 1+-- >>> odd_vartime 1 -- True--- >>> odd 2+-- >>> odd_vartime 2 -- False--- >>> Data.Word.Wider.odd (retr 3) -- parity is preserved--- True odd_vartime :: Montgomery -> Bool odd_vartime (Montgomery m) = C.decide (odd# m) @@ -856,7 +861,7 @@ select# = WW.select# {-# INLINE select# #-} --- | Return a if c is truthy, otherwise return b.+-- | Return b if c is truthy, otherwise return a. -- -- >>> import qualified Data.Choice as C -- >>> select 0 1 (C.true# ())
ppad-fixed.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-fixed-version: 0.1.6+version: 0.2.2 synopsis: Large fixed-width words and constant-time arithmetic. license: MIT license-file: LICENSE@@ -31,6 +31,11 @@ -Wall if flag(llvm) ghc-options: -fllvm -O2+ -- LLVM's x86 backend rewrites a cmov whose operand is a load as a+ -- branch (its cmov converter), which makes sub_mod#'s borrow-masked+ -- correction a branch on the borrow; keep the cmov.+ if flag(llvm) && arch(x86_64)+ ghc-options: -optlc=-x86-cmov-converter=false exposed-modules: Data.Choice , Data.Word.Limb
test/Montgomery/Curve.hs view
@@ -118,8 +118,23 @@ 0x00000000000000000000000000000000000000000000000000000001000003D1 0x000000000000000000000000000000000000000000000001000007A2000E90A1 +signum_correct :: H.Assertion+signum_correct = do+ H.assertBool "signum 0 = 0" (C.eq_vartime (signum 0) 0)+ H.assertBool "signum 5 = 1" (C.eq_vartime (signum 5) 1)+ H.assertBool "abs 5 * signum 5 = 5"+ (C.eq_vartime (abs 5 * signum 5) 5)++odd_witness :: H.Assertion+odd_witness = do+ -- 2 ^ 224 is even, but its montgomery form is odd+ H.assertBool "2 ^ 224 is even" (not (C.odd_vartime (2 ^ (224 :: Int))))+ H.assertBool "2 ^ 224 + 1 is odd" (C.odd_vartime (2 ^ (224 :: Int) + 1))++-- the whole 256-bit range, not just small values instance Q.Arbitrary W.Wider where- arbitrary = fmap W.to_vartime Q.arbitrary+ arbitrary =+ fmap W.to_vartime (Q.chooseInteger (0, 2 ^ (256 :: Int) - 1)) instance Q.Arbitrary C.Montgomery where arbitrary = fmap C.to Q.arbitrary@@ -171,17 +186,30 @@ odd_correct w = C.odd_vartime w == I.integerTestBit (W.from_vartime (C.from w)) 0 +-- integers of either sign, well outside the domain+fromInteger_matches :: Q.Property+fromInteger_matches =+ let b = 2 ^ (512 :: Int)+ in Q.forAll (Q.chooseInteger (negate b, b)) $ \k ->+ W.eq_vartime+ (W.to_vartime (k `mod` W.from_vartime m))+ (C.from (fromInteger k))+ tests :: TestTree tests = testGroup "montgomery tests (curve)" [ H.testCase "representation" repr , H.testCase "add" add , H.testCase "sub" sub , H.testCase "mul" mul+ , H.testCase "signum" signum_correct , Q.testProperty "a + b mod m ~ ma + mb" $ Q.withMaxSuccess 500 add_matches , Q.testProperty "a * b mod m ~ ma * mb" $ Q.withMaxSuccess 500 mul_matches , Q.testProperty "a ^ 2 mod m ~ ma ^ 2" $ Q.withMaxSuccess 500 sqr_matches , Q.testProperty "a ^ b mod m ~ ma ^ mb" $ Q.withMaxSuccess 500 exp_matches , Q.testProperty "n ^ -1 mod m * n ~ 1" $ Q.withMaxSuccess 500 inv_valid , Q.testProperty "odd m ~ odd (from m)" $ Q.withMaxSuccess 500 odd_correct+ , H.testCase "odd" odd_witness+ , Q.testProperty "fromInteger n ~ n mod m" $+ Q.withMaxSuccess 500 fromInteger_matches ]
test/Montgomery/Scalar.hs view
@@ -118,8 +118,23 @@ 0x000000000000000000000000000000014551231950B75FC4402DA1732FC9BEBF 0x9D671CD581C69BC5E697F5E45BCD07C6741496C20E7CF878896CF21467D7D140 +signum_correct :: H.Assertion+signum_correct = do+ H.assertBool "signum 0 = 0" (S.eq_vartime (signum 0) 0)+ H.assertBool "signum 5 = 1" (S.eq_vartime (signum 5) 1)+ H.assertBool "abs 5 * signum 5 = 5"+ (S.eq_vartime (abs 5 * signum 5) 5)++odd_witness :: H.Assertion+odd_witness = do+ -- 2 ^ 224 is even, but its montgomery form is odd+ H.assertBool "2 ^ 224 is even" (not (S.odd_vartime (2 ^ (224 :: Int))))+ H.assertBool "2 ^ 224 + 1 is odd" (S.odd_vartime (2 ^ (224 :: Int) + 1))++-- the whole 256-bit range, not just small values instance Q.Arbitrary W.Wider where- arbitrary = fmap W.to_vartime Q.arbitrary+ arbitrary =+ fmap W.to_vartime (Q.chooseInteger (0, 2 ^ (256 :: Int) - 1)) instance Q.Arbitrary S.Montgomery where arbitrary = fmap S.to Q.arbitrary@@ -167,16 +182,34 @@ inv_valid :: Q.NonZero S.Montgomery -> Bool inv_valid (Q.NonZero s) = S.eq_vartime (S.inv s * s) 1 +odd_correct :: S.Montgomery -> Bool+odd_correct w =+ S.odd_vartime w == I.integerTestBit (W.from_vartime (S.from w)) 0++-- integers of either sign, well outside the domain+fromInteger_matches :: Q.Property+fromInteger_matches =+ let b = 2 ^ (512 :: Int)+ in Q.forAll (Q.chooseInteger (negate b, b)) $ \k ->+ W.eq_vartime+ (W.to_vartime (k `mod` W.from_vartime m))+ (S.from (fromInteger k))+ tests :: TestTree tests = testGroup "montgomery tests (scalar)" [ H.testCase "representation" repr , H.testCase "add" add , H.testCase "sub" sub , H.testCase "mul" mul+ , H.testCase "signum" signum_correct , Q.testProperty "a + b mod m ~ ma + mb" $ Q.withMaxSuccess 500 add_matches , Q.testProperty "a * b mod m ~ ma * mb" $ Q.withMaxSuccess 500 mul_matches , Q.testProperty "a ^ 2 mod m ~ ma ^ 2" $ Q.withMaxSuccess 500 sqr_matches , Q.testProperty "a ^ b mod m ~ ma ^ mb" $ Q.withMaxSuccess 500 exp_matches , Q.testProperty "n ^ -1 mod m * n ~ 1" $ Q.withMaxSuccess 500 inv_valid+ , Q.testProperty "odd m ~ odd (from m)" $ Q.withMaxSuccess 500 odd_correct+ , H.testCase "odd" odd_witness+ , Q.testProperty "fromInteger n ~ n mod m" $+ Q.withMaxSuccess 500 fromInteger_matches ]
test/Wider.hs view
@@ -183,8 +183,27 @@ !e = 0x44acf6b7e36c1342c2c5897204fe09504e1e2efb1a900377dbc4e7a6a133ec56 H.assertBool mempty (W.eq_vartime o e) +shl1_c :: H.Assertion+shl1_c = case W.shl1_c (2 ^ (255 :: Word)) of+ (# r1, c1 #) -> case W.shl1_c 1 of+ (# r2, c2 #) -> do+ H.assertBool "value, carry out" (W.eq_vartime r1 0)+ H.assertBool "carry decides true" (C.decide c1)+ -- the carry must be a full-word mask, not a bare bit; negating+ -- or selecting on it is otherwise wrong+ H.assertBool "negated carry decides false"+ (not (C.decide (C.not c1)))+ H.assertBool "select on carry"+ (W.eq_vartime (W.select 0 1 c1) 1)+ H.assertBool "value, no carry" (W.eq_vartime r2 2)+ H.assertBool "no carry decides false" (not (C.decide c2))+ H.assertBool "select on no carry"+ (W.eq_vartime (W.select 0 1 c2) 0)++-- the whole 256-bit range, not just small values instance Q.Arbitrary W.Wider where- arbitrary = fmap W.to_vartime Q.arbitrary+ arbitrary =+ fmap W.to_vartime (Q.chooseInteger (0, 2 ^ (256 :: Int) - 1)) odd_correct :: W.Wider -> Bool odd_correct w = C.decide (W.odd w) == I.integerTestBit (W.from_vartime w) 0@@ -216,6 +235,7 @@ , H.testCase "sqr" sqr , H.testCase "mul" mul , H.testCase "sub_mod" sub_mod+ , H.testCase "shl1_c" shl1_c , Q.testProperty "odd w ~ odd (from w)" $ Q.withMaxSuccess 500 odd_correct , Q.testProperty "lt_vartime a b ~ from a < from b" $ Q.withMaxSuccess 500 lt_vartime_correct