Skip to content

Commit 10dc7de

Browse files
add an add-on package for recoverable signatures
1 parent 9c2304d commit 10dc7de

File tree

12 files changed

+550
-0
lines changed

12 files changed

+550
-0
lines changed

scripts/format

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ REPO_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd )
44

55
find $REPO_DIR/secp256k1-haskell/src -type f -name "*.hs" | xargs ormolu -i
66
find $REPO_DIR/secp256k1-haskell/test -type f -name "*.hs" | xargs ormolu -i
7+
find $REPO_DIR/secp256k1-haskell-recovery/src -type f -name "*.hs" | xargs ormolu -i
8+
find $REPO_DIR/secp256k1-haskell-recovery/test -type f -name "*.hs" | xargs ormolu -i
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6+
7+
## 0.1.0
8+
Initial version

secp256k1-haskell-recovery/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright 2020 Haskoin Developers
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
this software and associated documentation files (the "Software"), to deal in
5+
the Software without restriction, including without limitation the rights to
6+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
the Software, and to permit persons to whom the Software is furnished to do so,
8+
subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+

secp256k1-haskell-recovery/Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: secp256k1-haskell-recovery
2+
version: 0.1.0
3+
synopsis: Bindings for recoverable signatures feature of secp256k1
4+
description: Sign and verify recoverable signatures using the secp256k1 library.
5+
category: Crypto
6+
author: Evgeny Osipenko
7+
maintainer: [email protected]
8+
copyright: (c) 2023 Evgeny Osipenko
9+
license: MIT
10+
license-file: LICENSE
11+
github: haskoin/secp256k1-haskell
12+
homepage: http://github.com/haskoin/secp256k1-haskell#readme
13+
extra-source-files:
14+
- CHANGELOG.md
15+
dependencies:
16+
- base >=4.9 && <5
17+
- base16 >=1.0
18+
- bytestring >=0.10.8 && <0.12
19+
- entropy >=0.3.8 && <0.5
20+
- deepseq >=1.4.2 && <1.5
21+
- hashable >=1.2.6 && <1.5
22+
- QuickCheck >=2.9.2 && <2.15
23+
- secp256k1-haskell
24+
- string-conversions >=0.4 && <0.5
25+
- unliftio-core >=0.1.0 && <0.3
26+
library:
27+
source-dirs: src
28+
tests:
29+
spec:
30+
main: Spec.hs
31+
source-dirs: test
32+
ghc-options:
33+
- -threaded
34+
- -rtsopts
35+
- -with-rtsopts=-N
36+
verbatim:
37+
build-tool-depends:
38+
hspec-discover:hspec-discover
39+
dependencies:
40+
- hspec
41+
- secp256k1-haskell-recovery
42+
- monad-par
43+
- mtl
44+
- HUnit
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
cabal-version: 1.12
2+
3+
-- This file has been generated from package.yaml by hpack version 0.36.0.
4+
--
5+
-- see: https://github.com/sol/hpack
6+
7+
name: secp256k1-haskell-recovery
8+
version: 0.1.0
9+
synopsis: Bindings for recoverable signatures feature of secp256k1
10+
description: Sign and verify recoverable signatures using the secp256k1 library.
11+
category: Crypto
12+
homepage: http://github.com/haskoin/secp256k1-haskell#readme
13+
bug-reports: https://github.com/haskoin/secp256k1-haskell/issues
14+
author: Evgeny Osipenko
15+
maintainer: [email protected]
16+
copyright: (c) 2023 Evgeny Osipenko
17+
license: MIT
18+
license-file: LICENSE
19+
build-type: Simple
20+
extra-source-files:
21+
CHANGELOG.md
22+
23+
source-repository head
24+
type: git
25+
location: https://github.com/haskoin/secp256k1-haskell
26+
27+
library
28+
exposed-modules:
29+
Crypto.Secp256k1.Internal.Recovery
30+
Crypto.Secp256k1.Internal.RecoveryOps
31+
Crypto.Secp256k1.Recovery
32+
other-modules:
33+
Paths_secp256k1_haskell_recovery
34+
hs-source-dirs:
35+
src
36+
build-depends:
37+
QuickCheck >=2.9.2 && <2.15
38+
, base >=4.9 && <5
39+
, base16 >=1.0
40+
, bytestring >=0.10.8 && <0.12
41+
, deepseq >=1.4.2 && <1.5
42+
, entropy >=0.3.8 && <0.5
43+
, hashable >=1.2.6 && <1.5
44+
, secp256k1-haskell
45+
, string-conversions ==0.4.*
46+
, unliftio-core >=0.1.0 && <0.3
47+
default-language: Haskell2010
48+
49+
test-suite spec
50+
type: exitcode-stdio-1.0
51+
main-is: Spec.hs
52+
other-modules:
53+
Crypto.Secp256k1.RecoverySpec
54+
Paths_secp256k1_haskell_recovery
55+
hs-source-dirs:
56+
test
57+
ghc-options: -threaded -rtsopts -with-rtsopts=-N
58+
build-depends:
59+
HUnit
60+
, QuickCheck >=2.9.2 && <2.15
61+
, base >=4.9 && <5
62+
, base16 >=1.0
63+
, bytestring >=0.10.8 && <0.12
64+
, deepseq >=1.4.2 && <1.5
65+
, entropy >=0.3.8 && <0.5
66+
, hashable >=1.2.6 && <1.5
67+
, hspec
68+
, monad-par
69+
, mtl
70+
, secp256k1-haskell
71+
, secp256k1-haskell-recovery
72+
, string-conversions ==0.4.*
73+
, unliftio-core >=0.1.0 && <0.3
74+
default-language: Haskell2010
75+
build-tool-depends: hspec-discover:hspec-discover
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
{-# LANGUAGE DeriveGeneric #-}
2+
{-# LANGUAGE DuplicateRecordFields #-}
3+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+
{-# LANGUAGE ImportQualifiedPost #-}
5+
{-# LANGUAGE OverloadedRecordDot #-}
6+
{-# LANGUAGE NoFieldSelectors #-}
7+
8+
-- |
9+
-- Module : Crypto.Secp256k1.Internal.Recovery
10+
-- License : UNLICENSE
11+
-- Maintainer : Jean-Pierre Rupp <[email protected]>
12+
-- Stability : experimental
13+
-- Portability : POSIX
14+
--
15+
-- Crytpographic functions related to recoverable signatures from Bitcoin’s secp256k1 library.
16+
--
17+
-- The API for this module may change at any time. This is an internal module only
18+
-- exposed for hacking and experimentation.
19+
module Crypto.Secp256k1.Internal.Recovery where
20+
21+
import Control.DeepSeq (NFData)
22+
import Control.Monad (unless, (<=<))
23+
import Crypto.Secp256k1.Internal.Base (Msg (..), PubKey (..), SecKey (..), Sig (..))
24+
import Crypto.Secp256k1.Internal.Context (Ctx (..))
25+
import Crypto.Secp256k1.Internal.ForeignTypes
26+
( isSuccess,
27+
)
28+
import Crypto.Secp256k1.Internal.RecoveryOps
29+
( ecdsaRecover,
30+
ecdsaRecoverableSignatureConvert,
31+
ecdsaRecoverableSignatureParseCompact,
32+
ecdsaRecoverableSignatureSerializeCompact,
33+
ecdsaSignRecoverable,
34+
)
35+
import Crypto.Secp256k1.Internal.Util
36+
( decodeHex,
37+
showsHex,
38+
unsafePackByteString,
39+
unsafeUseByteString,
40+
)
41+
import Data.ByteString (ByteString)
42+
import Data.ByteString qualified as BS
43+
import Data.Maybe (fromMaybe)
44+
import Data.String (IsString (..))
45+
import Data.Word (Word8)
46+
import Foreign
47+
( alloca,
48+
free,
49+
mallocBytes,
50+
nullFunPtr,
51+
nullPtr,
52+
peek,
53+
)
54+
import GHC.Generics (Generic)
55+
import System.IO.Unsafe (unsafePerformIO)
56+
import Text.Read
57+
( Lexeme (String),
58+
lexP,
59+
parens,
60+
pfail,
61+
readPrec,
62+
)
63+
64+
newtype RecSig = RecSig {get :: ByteString}
65+
deriving (Eq, Generic, NFData)
66+
67+
data CompactRecSig = CompactRecSig
68+
{ rs :: !ByteString,
69+
v :: {-# UNPACK #-} !Word8
70+
}
71+
deriving (Eq, Generic)
72+
73+
instance NFData CompactRecSig
74+
75+
compactRecSig :: ByteString -> Maybe CompactRecSig
76+
compactRecSig bs
77+
| BS.length bs == 65,
78+
BS.last bs <= 3 =
79+
Just (CompactRecSig (BS.take 64 bs) (BS.last bs))
80+
| otherwise = Nothing
81+
82+
serializeCompactRecSig :: CompactRecSig -> ByteString
83+
serializeCompactRecSig (CompactRecSig bs v) =
84+
BS.snoc bs v
85+
86+
compactRecSigFromString :: String -> Maybe CompactRecSig
87+
compactRecSigFromString = compactRecSig <=< decodeHex
88+
89+
instance Read CompactRecSig where
90+
readPrec = parens $ do
91+
String str <- lexP
92+
maybe pfail return $ compactRecSigFromString str
93+
94+
instance IsString CompactRecSig where
95+
fromString = fromMaybe e . compactRecSigFromString
96+
where
97+
e = error "Could not decode signature from hex string"
98+
99+
instance Show CompactRecSig where
100+
showsPrec _ = showsHex . serializeCompactRecSig
101+
102+
-- | Parse a compact ECDSA signature (64 bytes + recovery id).
103+
importCompactRecSig :: Ctx -> CompactRecSig -> Maybe RecSig
104+
importCompactRecSig (Ctx ctx) (CompactRecSig sig_rs sig_v)
105+
| BS.length sig_rs == 64,
106+
sig_v <= 3 = unsafePerformIO $
107+
unsafeUseByteString sig_rs $ \(sig_rs_ptr, _) -> do
108+
out_rec_sig_ptr <- mallocBytes 65
109+
ret <-
110+
ecdsaRecoverableSignatureParseCompact
111+
ctx
112+
out_rec_sig_ptr
113+
sig_rs_ptr
114+
(fromIntegral sig_v)
115+
if isSuccess ret
116+
then do
117+
out_bs <- unsafePackByteString (out_rec_sig_ptr, 65)
118+
return (Just (RecSig out_bs))
119+
else do
120+
free out_rec_sig_ptr
121+
return Nothing
122+
| otherwise = Nothing
123+
124+
-- | Serialize an ECDSA signature in compact format (64 bytes + recovery id).
125+
exportCompactRecSig :: Ctx -> RecSig -> CompactRecSig
126+
exportCompactRecSig (Ctx ctx) (RecSig rec_sig_bs) = unsafePerformIO $
127+
unsafeUseByteString rec_sig_bs $ \(rec_sig_ptr, _) ->
128+
alloca $ \out_v_ptr -> do
129+
out_sig_ptr <- mallocBytes 64
130+
ret <-
131+
ecdsaRecoverableSignatureSerializeCompact
132+
ctx
133+
out_sig_ptr
134+
out_v_ptr
135+
rec_sig_ptr
136+
unless (isSuccess ret) $ do
137+
free out_sig_ptr
138+
error "Could not obtain compact signature"
139+
out_bs <- unsafePackByteString (out_sig_ptr, 64)
140+
out_v <- peek out_v_ptr
141+
return $ CompactRecSig out_bs (fromIntegral out_v)
142+
143+
-- | Convert a recoverable signature into a normal signature.
144+
convertRecSig :: Ctx -> RecSig -> Sig
145+
convertRecSig (Ctx ctx) (RecSig rec_sig_bs) = unsafePerformIO $
146+
unsafeUseByteString rec_sig_bs $ \(rec_sig_ptr, _) -> do
147+
out_ptr <- mallocBytes 64
148+
ret <- ecdsaRecoverableSignatureConvert ctx out_ptr rec_sig_ptr
149+
unless (isSuccess ret) $
150+
error "Could not convert a recoverable signature"
151+
out_bs <- unsafePackByteString (out_ptr, 64)
152+
return $ Sig out_bs
153+
154+
-- | Create a recoverable ECDSA signature.
155+
signRecMsg :: Ctx -> SecKey -> Msg -> RecSig
156+
signRecMsg (Ctx ctx) (SecKey sec_key) (Msg m) = unsafePerformIO $
157+
unsafeUseByteString sec_key $ \(sec_key_ptr, _) ->
158+
unsafeUseByteString m $ \(msg_ptr, _) -> do
159+
rec_sig_ptr <- mallocBytes 65
160+
ret <- ecdsaSignRecoverable ctx rec_sig_ptr msg_ptr sec_key_ptr nullFunPtr nullPtr
161+
unless (isSuccess ret) $ do
162+
free rec_sig_ptr
163+
error "could not sign message"
164+
RecSig <$> unsafePackByteString (rec_sig_ptr, 65)
165+
166+
-- | Recover an ECDSA public key from a signature.
167+
recover :: Ctx -> RecSig -> Msg -> Maybe PubKey
168+
recover (Ctx ctx) (RecSig rec_sig) (Msg m) = unsafePerformIO $
169+
unsafeUseByteString rec_sig $ \(rec_sig_ptr, _) ->
170+
unsafeUseByteString m $ \(msg_ptr, _) -> do
171+
pub_key_ptr <- mallocBytes 64
172+
ret <- ecdsaRecover ctx pub_key_ptr rec_sig_ptr msg_ptr
173+
if isSuccess ret
174+
then do
175+
pub_key_bs <- unsafePackByteString (pub_key_ptr, 64)
176+
return (Just (PubKey pub_key_bs))
177+
else do
178+
free pub_key_ptr
179+
return Nothing
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
-- |
2+
-- Module : Crypto.Secp256k1.Internal.RecoveryOps
3+
-- License : UNLICENSE
4+
-- Maintainer : Jean-Pierre Rupp <[email protected]>
5+
-- Stability : experimental
6+
-- Portability : POSIX
7+
--
8+
-- The API for this module may change at any time. This is an internal module only
9+
-- exposed for hacking and experimentation.
10+
module Crypto.Secp256k1.Internal.RecoveryOps where
11+
12+
import Crypto.Secp256k1.Internal.ForeignTypes (Compact64, LCtx, Msg32, NonceFun, PubKey64, Ret, SecKey32, Sig64)
13+
import Foreign (FunPtr, Ptr)
14+
import Foreign.C (CInt (..))
15+
16+
data RecSig65
17+
18+
foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_parse_compact"
19+
ecdsaRecoverableSignatureParseCompact ::
20+
Ptr LCtx ->
21+
Ptr RecSig65 ->
22+
Ptr Compact64 ->
23+
CInt ->
24+
IO Ret
25+
26+
foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_convert"
27+
ecdsaRecoverableSignatureConvert ::
28+
Ptr LCtx ->
29+
Ptr Sig64 ->
30+
Ptr RecSig65 ->
31+
IO Ret
32+
33+
foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_serialize_compact"
34+
ecdsaRecoverableSignatureSerializeCompact ::
35+
Ptr LCtx ->
36+
Ptr Compact64 ->
37+
Ptr CInt ->
38+
Ptr RecSig65 ->
39+
IO Ret
40+
41+
foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_sign_recoverable"
42+
ecdsaSignRecoverable ::
43+
Ptr LCtx ->
44+
Ptr RecSig65 ->
45+
Ptr Msg32 ->
46+
Ptr SecKey32 ->
47+
FunPtr (NonceFun a) ->
48+
-- | nonce data
49+
Ptr a ->
50+
IO Ret
51+
52+
foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recover"
53+
ecdsaRecover ::
54+
Ptr LCtx ->
55+
Ptr PubKey64 ->
56+
Ptr RecSig65 ->
57+
Ptr Msg32 ->
58+
IO Ret

0 commit comments

Comments
 (0)