Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECDSA with Public Key Recovery #43

Merged
merged 11 commits into from
Mar 5, 2025
18 changes: 17 additions & 1 deletion Crypto/Number/F2m.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ module Crypto.Number.F2m (
sqrtF2m,
invF2m,
divF2m,
quadraticF2m,
) where

import Crypto.Number.Basic
import Data.Bits (setBit, shift, testBit, xor)
import Data.Bits (setBit, shift, testBit, xor, unsafeShiftR)
import Data.List

-- | Binary Polynomial represented by an integer
Expand Down Expand Up @@ -209,3 +210,18 @@ divF2m
-- ^ Quotient
divF2m fx n1 n2 = mulF2m fx n1 <$> invF2m fx n2
{-# INLINE divF2m #-}

traceF2m :: BinaryPolynomial -> Integer -> Integer
traceF2m fx = foldr addF2m 0 . take (log2 fx) . iterate (squareF2m fx)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that a pair of take and iterate should be replaced with replicate.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are defining a list of iterated squares:

[fx, squareF2m fx, squareF2m (squareF2m fx), squareF2m (squareF2m (squareF2m fx)), ...]

And then we take the sum (foldr addF2m 0) of a prefix of that list. I do not know how to do this with replicate, since that would just give us the last item of that prefix.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I took it as repeat.

{-# INLINE traceF2m #-}

halfTraceF2m :: BinaryPolynomial -> Integer -> Integer
halfTraceF2m fx = foldr addF2m 0 . take (1 + log2 fx `unsafeShiftR` 1) . iterate (squareF2m fx . squareF2m fx)
{-# INLINE halfTraceF2m #-}

-- | Solve a quadratic equation of the form @x^2 + x = a@ in F₂m.
quadraticF2m :: BinaryPolynomial -> Integer -> Maybe Integer
quadraticF2m fx a
| traceF2m fx a == 0 = Just $ halfTraceF2m fx a
| otherwise = Nothing
{-# INLINABLE quadraticF2m #-}
78 changes: 65 additions & 13 deletions Crypto/PubKey/ECC/ECDSA.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
-- should be safe.
module Crypto.PubKey.ECC.ECDSA (
Signature (..),
ExtendedSignature (..),
PublicPoint,
PublicKey (..),
PrivateNumber,
Expand All @@ -13,10 +14,14 @@ module Crypto.PubKey.ECC.ECDSA (
toPrivateKey,
signWith,
signDigestWith,
signExtendedDigestWith,
sign,
signDigest,
signExtendedDigest,
verify,
verifyDigest,
recover,
recoverDigest,
) where

import Control.Monad
Expand All @@ -40,6 +45,17 @@ data Signature = Signature
}
deriving (Show, Read, Eq, Data)

-- | ECDSA signature with public key recovery information.
data ExtendedSignature = ExtendedSignature
{ index :: Integer
-- ^ Index of the X coordinate
, parity :: Bool
-- ^ Parity of the Y coordinate
, signature :: Signature
-- ^ Inner signature
}
deriving (Show, Read, Eq, Data)

-- | ECDSA Private Key.
data PrivateKey = PrivateKey
{ private_curve :: Curve
Expand Down Expand Up @@ -69,26 +85,37 @@ toPrivateKey (KeyPair curve _ priv) = PrivateKey curve priv
-- | Sign digest using the private key and an explicit k number.
--
-- /WARNING:/ Vulnerable to timing attacks.
signDigestWith
signExtendedDigestWith
:: HashAlgorithm hash
=> Integer
-- ^ k random number
-> PrivateKey
-- ^ private key
-> Digest hash
-- ^ digest to sign
-> Maybe Signature
signDigestWith k (PrivateKey curve d) digest = do
-> Maybe ExtendedSignature
signExtendedDigestWith k (PrivateKey curve d) digest = do
let z = dsaTruncHashDigest digest n
CurveCommon _ _ g n _ = common_curve curve
let point = pointMul curve k g
r <- case point of
PointO -> Nothing
Point x _ -> return $ x `mod` n
(i, r, p) <- pointDecompose curve $ pointMul curve k g
kInv <- inverse k n
let s = kInv * (z + r * d) `mod` n
when (r == 0 || s == 0) Nothing
return $ Signature r s
return $ ExtendedSignature i p $ Signature r s

-- | Sign digest using the private key and an explicit k number.
--
-- /WARNING:/ Vulnerable to timing attacks.
signDigestWith
:: HashAlgorithm hash
=> Integer
-- ^ k random number
-> PrivateKey
-- ^ private key
-> Digest hash
-- ^ digest to sign
-> Maybe Signature
signDigestWith k pk digest = signature <$> signExtendedDigestWith k pk digest

-- | Sign message using the private key and an explicit k number.
--
Expand All @@ -109,17 +136,25 @@ signWith k pk hashAlg msg = signDigestWith k pk (hashWith hashAlg msg)
-- | Sign digest using the private key.
--
-- /WARNING:/ Vulnerable to timing attacks.
signDigest
signExtendedDigest
:: (HashAlgorithm hash, MonadRandom m)
=> PrivateKey -> Digest hash -> m Signature
signDigest pk digest = do
=> PrivateKey -> Digest hash -> m ExtendedSignature
signExtendedDigest pk digest = do
k <- generateBetween 1 (n - 1)
case signDigestWith k pk digest of
Nothing -> signDigest pk digest
case signExtendedDigestWith k pk digest of
Nothing -> signExtendedDigest pk digest
Just sig -> return sig
where
n = ecc_n . common_curve $ private_curve pk

-- | Sign digest using the private key.
--
-- /WARNING:/ Vulnerable to timing attacks.
signDigest
:: (HashAlgorithm hash, MonadRandom m)
=> PrivateKey -> Digest hash -> m Signature
signDigest pk digest = signature <$> signExtendedDigest pk digest

-- | Sign message using the private key.
--
-- /WARNING:/ Vulnerable to timing attacks.
Expand Down Expand Up @@ -153,3 +188,20 @@ verify
:: (ByteArrayAccess msg, HashAlgorithm hash)
=> hash -> PublicKey -> Signature -> msg -> Bool
verify hashAlg pk sig msg = verifyDigest pk sig (hashWith hashAlg msg)

-- | Recover the public key from an extended signature and a digest.
recoverDigest
:: HashAlgorithm hash
=> Curve -> ExtendedSignature -> Digest hash -> Maybe PublicKey
recoverDigest curve (ExtendedSignature i p (Signature r s)) digest = do
let CurveCommon _ _ g n _ = common_curve curve
let z = dsaTruncHashDigest digest n
w <- inverse r n
c <- pointCompose curve i r p
pure $ PublicKey curve $ pointAddTwoMuls curve (s * w) c (negate $ z * w) g

-- | Recover the public key from an extended signature and a message.
recover
:: (ByteArrayAccess msg, HashAlgorithm hash)
=> hash -> Curve -> ExtendedSignature -> msg -> Maybe PublicKey
recover hashAlg curve sig msg = recoverDigest curve sig $ hashWith hashAlg msg
34 changes: 34 additions & 0 deletions Crypto/PubKey/ECC/Prim.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module Crypto.PubKey.ECC.Prim (
pointBaseMul,
pointMul,
pointAddTwoMuls,
pointDecompose,
pointCompose,
isPointAtInfinity,
isPointValid,
) where
Expand Down Expand Up @@ -138,6 +140,38 @@ pointAddTwoMuls c n1 p1 n2 p2
(False, True) -> pointAdd c p2 q
(False, False) -> q

-- | Decompose a point into index, residue, and parity.
--
-- Adapted from SEC 1: Elliptic Curve Cryptography, Version 2.0, section 2.3.3.
pointDecompose :: Curve -> Point -> Maybe (Integer, Integer, Bool)
pointDecompose _ PointO = Nothing
pointDecompose curve (Point x y) = do
let CurveCommon _ _ _ n _ = common_curve curve
let (index, residue) = x `divMod` n
parity <- case curve of
CurveFP _ -> pure $ odd y
CurveF2m _ | x == 0 -> pure False
CurveF2m (CurveBinary fx _) -> odd <$> divF2m fx y x
pure (index, residue, parity)

-- | Compose a point from index, residue, and parity.
--
-- Adapted from SEC 1: Elliptic Curve Cryptography, Version 2.0, section 2.3.4.
pointCompose :: Curve -> Integer -> Integer -> Bool -> Maybe Point
pointCompose curve index residue parity = do
let CurveCommon a b _ n _ = common_curve curve
let x = residue + index * n
y <- case curve of
CurveFP (CurvePrime p _) -> do
z <- squareRoot p $ x ^ (3 :: Int) + a * x + b
pure $ if odd z == parity then z else p - z
CurveF2m (CurveBinary fx _) | x == 0 -> pure $ sqrtF2m fx b
CurveF2m (CurveBinary fx _) -> do
c <- divF2m fx b $ squareF2m fx x
z <- quadraticF2m fx $ addF2m x $ addF2m a c
pure $ mulF2m fx x $ if odd z == parity then z else addF2m 1 z
pure $ Point x y

-- | Check if a point is the point at infinity.
isPointAtInfinity :: Point -> Bool
isPointAtInfinity PointO = True
Expand Down
31 changes: 27 additions & 4 deletions tests/ECDSA.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ module ECDSA (tests) where

import qualified Crypto.ECC as ECDSA
import Crypto.Error
import Crypto.Hash.Algorithms
import Crypto.Hash
import qualified Crypto.PubKey.ECC.Generate as ECC
import qualified Crypto.PubKey.ECC.ECDSA as ECC
import qualified Crypto.PubKey.ECC.Types as ECC
import qualified Crypto.PubKey.ECDSA as ECDSA
Expand Down Expand Up @@ -43,16 +44,38 @@ sigECCToECDSA prx (ECC.Signature r s) =
(throwCryptoError $ ECDSA.scalarFromInteger prx r)
(throwCryptoError $ ECDSA.scalarFromInteger prx s)

tests =
localOption (QuickCheckTests 5) $
testRecover :: ECC.CurveName -> TestTree
testRecover name = testProperty (show name) $ \ (ArbitraryBS0_2901 msg) -> do
let curve = ECC.getCurveByName name
let n = ECC.ecc_n $ ECC.common_curve curve
k <- choose (1, n - 1)
d <- choose (1, n - 1)
let key = ECC.PrivateKey curve d
let digest = hashWith SHA256 msg
let pub = ECC.signExtendedDigestWith k key digest >>= \ signature -> ECC.recoverDigest curve signature digest
pure $ propertyHold [eqTest "recovery" (Just $ ECC.generateQ curve d) (ECC.public_q <$> pub)]

tests = testGroup "ECDSA"
[ localOption (QuickCheckTests 5) $
testGroup
"ECDSA"
"verification"
[ testProperty "SHA1" $ propertyECDSA SHA1
, testProperty "SHA224" $ propertyECDSA SHA224
, testProperty "SHA256" $ propertyECDSA SHA256
, testProperty "SHA384" $ propertyECDSA SHA384
, testProperty "SHA512" $ propertyECDSA SHA512
]
, testGroup "recovery"
[ localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p128r1
, localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p128r2
, localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p256k1
, localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p256r1
, localOption (QuickCheckTests 50) $ testRecover ECC.SEC_t131r1
, localOption (QuickCheckTests 50) $ testRecover ECC.SEC_t131r2
, localOption (QuickCheckTests 20) $ testRecover ECC.SEC_t233k1
, localOption (QuickCheckTests 20) $ testRecover ECC.SEC_t233r1
]
]
where
propertyECDSA hashAlg (Curve c curve _) (ArbitraryBS0_2901 msg) = do
d <- arbitraryScalar curve
Expand Down
Loading