Skip to content

Commit

Permalink
support v parameter for secp256k1 key recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
larskuhtz committed Nov 15, 2023
1 parent 3c57254 commit f84df5f
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 17 deletions.
98 changes: 86 additions & 12 deletions src-secp256k1/Crypto/Secp256k1.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module Crypto.Secp256k1
, ecdsaR
, EcdsaS
, ecdsaS
, EcdsaV
, ecdsaV
, ecdsaVerify
, ecdsaRecoverPublicKey
) where
Expand Down Expand Up @@ -82,9 +84,67 @@ publicKeyPointFromBytes bs = do
-- Public API

newtype EcdsaPublicKey = EcdsaPublicKey Point
deriving (Show, Eq)

newtype EcdsaMessageDigest = EcdsaMessageDigest Fn
deriving (Show, Eq)

newtype EcdsaR = EcdsaR Fn
deriving (Show, Eq)

newtype EcdsaS = EcdsaS Fn
deriving (Show, Eq)

-- | The recovery id or V value of an ECDSA signature is used for public key
-- recovery. If present it indicates the parity of the y-coordinate of the
-- public key and whether the magnitude of the x-coordinate is lower than the curve order.
--
-- In Ethereum a value is originally computed as
-- \(27 + y-parity + (magnitude of x is lower curve order)\).
--
-- Since EIP-155 the value is computed for the Ethereum mainnet as
-- \(37 + y-parity + (magnitude of x is lower curve order)\).
--
-- Mainy external tools still use the original values and implementations
-- should therefore support both options.
--
-- Note, that the values 29, 30, 39, and 40 are extremly unlikely to occur with
-- randomly generate keys.
--
-- Also note, that the V value may not always be provided with the signature
-- in which case it is safe to just try all possible values when recovering
-- the public key.
--
data EcdsaV = EcdsaV
!Bool
-- ^ whether parity of the y-coordinate of public recovered public key
-- is odd.
--
-- If you don't know this parameter it is safe to try both options, at
-- the cost of taking on average 1.5 times more computation time to
-- compute the result.

!Bool
-- ^ whether the second solution for the public key is returned. This
-- parameter is almost surely always @False@.
--
-- If you don't know this value it is safe to assume that it is @False@.
--
-- https://www.secg.org/sec1-v2.pdf, 4.1.3:
--
-- The publicly verifiable criteria that r may be conditioned to satisfy may
-- include that xR is uniquely recoverable from r in that only one of the
-- integers \(xR = r + jn\) for \(j ∈ {0, 1, 2, ..., h}\) represents a valid
-- x-coordinate of a multiple of G. For the recommended curves [SEC 2] with h =
-- 1 and h = 2, the number of valid candidate x-coordinates is usually one, so
-- this is a vacuous check.
--
-- "Usually" here means something like always except for one out of \(2^{128}\).
-- However, in the context of a public blockchain, an attack may be able to
-- fabricate a respective signature and cause diverging behavior between
-- validating nodes with different implementations for handling this corner
-- case. Although, it is not clear whether creating such an attack is infeasible.
deriving (Show, Eq)

-- | Input: 65 bytes that represent an uncompressed (prefix 0x04) secp256k1
-- curve point.
Expand Down Expand Up @@ -120,6 +180,26 @@ ecdsaR = fmap (EcdsaR . shortBytesToFn) . checkLength "ecdsaR" 32
ecdsaS :: MonadThrow m => BS.ShortByteString -> m EcdsaS
ecdsaS = fmap (EcdsaS . shortBytesToFn) . checkLength "ecdsaS" 32

-- | Input: 1 byte long V value of the secp256k1 ECDSA signature
--
ecdsaV :: MonadThrow m => BS.ShortByteString -> m EcdsaV
ecdsaV = checkLength "ecdsaV" 1 >=> \case

-- before EIP-155
"\27" -> return $ EcdsaV False False
"\28" -> return $ EcdsaV True False
"\29" -> return $ EcdsaV False True
"\30" -> return $ EcdsaV True True

-- EIP-155
"\37" -> return $ EcdsaV False False
"\38" -> return $ EcdsaV True False
"\39" -> return $ EcdsaV False True
"\40" -> return $ EcdsaV True True

e -> throwM $ EcdsaException $
"Invalid V value for signature: " <> sshow e

ecdsaVerify
:: MonadThrow m
=> EcdsaMessageDigest
Expand All @@ -146,19 +226,13 @@ ecdsaRecoverPublicKey
-- ^ The R value of the input signature
-> EcdsaS
-- ^ The S value of the input singature
-> Bool
-- ^ whether parity of the public recovered public key is odd.
--
-- If you don't know this parameter it is safe to try both options, at
-- the cost of taking on average 1.5 times more computation time to
-- compute the result.
-> Bool
-- ^ whether the second solution for the public key is returned. This
-- parameter is almost surely always @False@.
-> EcdsaV
-- ^ The recovery id or V value of the input singature
--
-- If you don't know this value it is safe to assume that it is @False@.
-- If you don't know this value you may just try all 4 possible value,
-- where the values with 'ecdsaVHigh' are extremely unlikely.
--
-> Maybe EcdsaPublicKey
ecdsaRecoverPublicKey (EcdsaMessageDigest d) (EcdsaR r) (EcdsaS s) oddY secondKey =
EcdsaPublicKey <$> recoverPublicKey d r s oddY secondKey
ecdsaRecoverPublicKey (EcdsaMessageDigest d) (EcdsaR r) (EcdsaS s) (EcdsaV isOddY isSecond) =
EcdsaPublicKey <$> recoverPublicKey d r s isOddY isSecond

113 changes: 108 additions & 5 deletions test-secp256k1/Test/Crypto/Secp256k1/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Short as BS
import Data.Coerce
import Data.Hash.SHA3
import Data.Word (Word8)

import GHC.TypeNats

Expand All @@ -40,6 +41,7 @@ import Test.Tasty.QuickCheck
-- internal modules

import Crypto.Secp256k1.Internal
import Crypto.Secp256k1

-- -------------------------------------------------------------------------- --
-- Examples
Expand All @@ -64,10 +66,14 @@ test_1_verify = verify h1 r1 s1 pk1 === Right True
test_1_recover :: Property
test_1_recover = recoverPublicKey h1 r1 s1 False False === Just pk1

test_2_recover :: Property
test_2_recover = recoverPublicKey h1 r1 s1 True False =/= Just pk1

properties_example1 :: TestTree
properties_example1 = testGroup "example1"
[ testProperty "test_1_verify" test_1_verify
, testProperty "test_1_recover" test_1_recover
, testProperty "test_2_recover" test_2_recover
]

sk2, k2, h2, r2, s2 :: Fn
Expand Down Expand Up @@ -150,8 +156,8 @@ hashMsg msg
-- -------------------------------------------------------------------------- --
-- ECDSA Properties

prop_ecdsa_verify :: Int -> Property
prop_ecdsa_verify msg = ioProperty $ do
prop_verify :: Int -> Property
prop_verify msg = ioProperty $ do
(sk, pk) <- genKey
(r, s, isOddY, isSecondKey) <- sign sk msgDigest
return
Expand All @@ -161,8 +167,8 @@ prop_ecdsa_verify msg = ioProperty $ do
where
msgDigest = hashMsg @Sha3_256 $ B8.pack $ show msg

prop_ecdsa_recover :: Int -> Property
prop_ecdsa_recover msg = ioProperty $ do
prop_recover :: Int -> Property
prop_recover msg = ioProperty $ do
(sk, pk) <- genKey
(r, s, isOddY, isSecondKey) <- sign sk msgDigest
return
Expand All @@ -180,8 +186,104 @@ prop_ecdsa_recover msg = ioProperty $ do

properties_ecdsa :: TestTree
properties_ecdsa = testGroup "ECDSA"
[ testProperty "prop_verify" prop_verify
, testProperty "prop_recover" prop_recover
]

-- -------------------------------------------------------------------------- --
-- Public ECDSA API

ecdsaGenKey :: IO (Fn, EcdsaPublicKey)
ecdsaGenKey = do
k <- genKey
traverse (ecdsaPublicKey . pointToBytes) k
where
pointToBytes :: Point -> BS.ShortByteString
pointToBytes (Point x y) = BS.cons 0x04 (fpToShortBytes x <> fpToShortBytes y)
pointToBytes O = BS.pack [0x00]

ecdsaSign_
:: Word8
-> Fn
-> B.ByteString
-> IO (EcdsaR, EcdsaS, EcdsaV)
ecdsaSign_ x sk msg = do
(r, s, isOddY, isSecondKey) <- sign sk (hashMsg @Sha3_256 msg)
(,,)
<$> ecdsaR (fnToShortBytes r)
<*> ecdsaS (fnToShortBytes s)
<*> ecdsaV (BS.singleton (x + if isOddY then 1 else 0 + if isSecondKey then 1 else 0))

ecdsaSignEip155
:: Fn
-> B.ByteString
-> IO (EcdsaR, EcdsaS, EcdsaV)
ecdsaSignEip155 = ecdsaSign_ 37

ecdsaSignOrig
:: Fn
-> B.ByteString
-> IO (EcdsaR, EcdsaS, EcdsaV)
ecdsaSignOrig = ecdsaSign_ 27

ecdsaHashMsg
:: forall h
. Hash h
=> Coercible h BS.ShortByteString
=> B.ByteString
-> EcdsaMessageDigest
ecdsaHashMsg msg = case ecdsaMessageDigest h of
Left e -> error (show e)
Right d -> d
where
h = coerce (hashByteString @h msg)

prop_ecdsa_verify :: Int -> Property
prop_ecdsa_verify msg = ioProperty $ do
(sk, pk) <- ecdsaGenKey
(r, s, _v) <- ecdsaSignEip155 sk msgBytes
return $ case ecdsaVerify msgDigest pk r s of
Right x -> x === True
Left e -> counterexample (show e) $ False
where
msgBytes = B8.pack $ show msg
msgDigest = ecdsaHashMsg @Sha3_256 msgBytes

prop_ecdsa_recover_orig :: Int -> Property
prop_ecdsa_recover_orig msg = ioProperty $ do
(sk, pk) <- ecdsaGenKey
(r, s, v) <- ecdsaSignOrig sk msgBytes
return
$ counterexample ("sk: " <> show sk)
$ counterexample ("msgDigest: " <> show msgDigest)
$ counterexample ("r: " <> show r)
$ counterexample ("s: " <> show s)
$ counterexample ("v: " <> show v)
$ ecdsaRecoverPublicKey msgDigest r s v === Just pk
where
msgBytes = B8.pack $ show msg
msgDigest = ecdsaHashMsg @Sha3_256 msgBytes

prop_ecdsa_recover_eip155 :: Int -> Property
prop_ecdsa_recover_eip155 msg = ioProperty $ do
(sk, pk) <- ecdsaGenKey
(r, s, v) <- ecdsaSignEip155 sk msgBytes
return
$ counterexample ("sk: " <> show sk)
$ counterexample ("msgDigest: " <> show msgDigest)
$ counterexample ("r: " <> show r)
$ counterexample ("s: " <> show s)
$ counterexample ("v: " <> show v)
$ ecdsaRecoverPublicKey msgDigest r s v === Just pk
where
msgBytes = B8.pack $ show msg
msgDigest = ecdsaHashMsg @Sha3_256 msgBytes

properties_ecdsa_api :: TestTree
properties_ecdsa_api = testGroup "ECDSA"
[ testProperty "prop_ecdsa_verify" prop_ecdsa_verify
, testProperty "prop_ecdsa_recover" prop_ecdsa_recover
, testProperty "prop_ecdsa_recover_orig" prop_ecdsa_recover_orig
, testProperty "prop_ecdsa_recover_eip155" prop_ecdsa_recover_eip155
]

-- -------------------------------------------------------------------------- --
Expand Down Expand Up @@ -363,5 +465,6 @@ tests = testGroup "Crypto.Secp256k1"
, properties_Fp_sqrt
, properties_P
, properties_ecdsa
, properties_ecdsa_api
, properties_example1
]

0 comments on commit f84df5f

Please sign in to comment.