Skip to content

Commit 9db4d62

Browse files
authored
Merge pull request #9 from prolic/v2
Add SchnorrSignature type with parsing and serialization
2 parents 1e4ebd2 + 10555f6 commit 9db4d62

File tree

4 files changed

+179
-23
lines changed

4 files changed

+179
-23
lines changed

libsecp256k1.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ library
4747
, hashable >=1.4.2 && <1.5
4848
, hedgehog >=1.2 && <1.5
4949
, memory >=0.14.15 && <1.0
50+
, random >=1.2.1.2 && <1.3
5051
, transformers >=0.4.0.0 && <1.0
5152
default-language: Haskell2010
5253

@@ -79,5 +80,6 @@ test-suite spec
7980
, libsecp256k1
8081
, memory >=0.14.15 && <1.0
8182
, monad-par
83+
, random >=1.2.1.2 && <1.3
8284
, transformers >=0.4.0.0 && <1.0
8385
default-language: Haskell2010

package.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies:
2121
- hashable >=1.4.2 && <1.5
2222
- hedgehog >= 1.2 && <1.5
2323
- memory >= 0.14.15 && <1.0
24+
- random >=1.2.1.2 && <1.3
2425
- transformers >= 0.4.0.0 && <1.0
2526
default-extensions:
2627
- ImportQualifiedPost

src/Crypto/Secp256k1.hs

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Crypto.Secp256k1 (
2525
KeyPair,
2626
Signature,
2727
RecoverableSignature,
28+
SchnorrSignature,
2829
Tweak,
2930

3031
-- * Parsing and Serialization
@@ -40,6 +41,8 @@ module Crypto.Secp256k1 (
4041
exportSignatureDer,
4142
importRecoverableSignature,
4243
exportRecoverableSignature,
44+
importSchnorrSignature,
45+
exportSchnorrSignature,
4346
importTweak,
4447

4548
-- * ECDSA Operations
@@ -73,6 +76,8 @@ module Crypto.Secp256k1 (
7376

7477
-- * Schnorr Operations
7578
schnorrSign,
79+
schnorrSignDeterministic,
80+
schnorrSignNondeterministic,
7681
schnorrVerify,
7782

7883
-- * Other
@@ -140,6 +145,7 @@ import Foreign.Storable (Storable (..))
140145
import GHC.Generics (Generic)
141146
import GHC.IO.Handle.Text (memcpy)
142147
import System.IO.Unsafe (unsafePerformIO)
148+
import System.Random (StdGen, newStdGen, randoms, randomIO)
143149
import Text.Read (
144150
Lexeme (String),
145151
lexP,
@@ -283,6 +289,28 @@ instance NFData Signature where
283289
rnf Signature{..} = seq signatureFPtr ()
284290

285291

292+
-- | Structure containing Schnorr Signature
293+
newtype SchnorrSignature = SchnorrSignature {schnorrSignatureFPtr :: ForeignPtr Prim.Sig64}
294+
295+
296+
instance Show SchnorrSignature where
297+
show sig = (B8.unpack . encodeBase16) (exportSchnorrSignature sig)
298+
instance Read SchnorrSignature where
299+
readsPrec i cs = case decodeBase16 $ B8.pack token of
300+
Left e -> []
301+
Right a -> maybeToList $ (,rest) <$> importSchnorrSignature a
302+
where
303+
trimmed = dropWhile isSpace cs
304+
(token, rest) = span isAlphaNum trimmed
305+
instance Eq SchnorrSignature where
306+
sig == sig' = unsafePerformIO . evalContT $ do
307+
sigp <- ContT $ withForeignPtr (schnorrSignatureFPtr sig)
308+
sigp' <- ContT $ withForeignPtr (schnorrSignatureFPtr sig')
309+
(EQ ==) <$> lift (memCompare (castPtr sigp) (castPtr sigp') 64)
310+
instance NFData SchnorrSignature where
311+
rnf SchnorrSignature{..} = seq schnorrSignatureFPtr ()
312+
313+
286314
-- | Structure containing Signature AND recovery ID
287315
newtype RecoverableSignature = RecoverableSignature {recoverableSignatureFPtr :: ForeignPtr Prim.RecSig65}
288316

@@ -493,6 +521,23 @@ exportRecoverableSignature RecoverableSignature{..} = unsafePerformIO . evalCont
493521
unsafePackByteString (outBuf, 65)
494522

495523

524+
-- | Parses 'SchnorrSignature' from Schnorr (64 byte) representation
525+
importSchnorrSignature :: ByteString -> Maybe SchnorrSignature
526+
importSchnorrSignature bs
527+
| BS.length bs /= 64 = Nothing
528+
| otherwise = unsafePerformIO $ do
529+
outBuf <- mallocBytes 64
530+
unsafeUseByteString bs $ \(ptr, _) -> do
531+
memcpy outBuf ptr 64
532+
Just . SchnorrSignature <$> newForeignPtr finalizerFree outBuf
533+
534+
535+
-- | Serializes 'SchnorrSignature' to Schnorr (64 byte) representation
536+
exportSchnorrSignature :: SchnorrSignature -> ByteString
537+
exportSchnorrSignature (SchnorrSignature fptr) = unsafePerformIO $
538+
withForeignPtr fptr $ \ptr -> BS.packCStringLen (castPtr ptr, 64)
539+
540+
496541
-- | Parses 'Tweak' from 32 byte @ByteString@. If the @ByteString@ is an invalid 'SecKey' then this will yield @Nothing@
497542
importTweak :: ByteString -> Maybe Tweak
498543
importTweak = fmap (Tweak . castForeignPtr . secKeyFPtr) . importSecKey
@@ -700,30 +745,47 @@ keyPairPubKeyXOTweakAdd KeyPair{..} Tweak{..} = unsafePerformIO . evalContT $ do
700745
else free keyPairOut $> Nothing
701746

702747

703-
-- | Compute a schnorr signature using a 'KeyPair'. The @ByteString@ must be 32 bytes long to get a @Just@ out of this
704-
-- function
705-
schnorrSign :: KeyPair -> ByteString -> Maybe Signature
706-
schnorrSign KeyPair{..} bs
748+
-- | Compute a schnorr signature using a 'KeyPair'. The @ByteString@ must be 32 bytes long to get
749+
-- a @Just@ out of this function. Optionally takes a 'StdGen' for deterministic signing.
750+
schnorrSign :: Maybe StdGen -> KeyPair -> ByteString -> Maybe SchnorrSignature
751+
schnorrSign mGen KeyPair{..} bs
707752
| BS.length bs /= 32 = Nothing
708753
| otherwise = unsafePerformIO . evalContT $ do
709754
(msgHashPtr, _) <- ContT (unsafeUseByteString bs)
710755
keyPairPtr <- ContT (withForeignPtr keyPairFPtr)
711756
lift $ do
712757
sigBuf <- mallocBytes 64
713-
-- TODO: provide randomness here instead of supplying a null pointer
714-
ret <- Prim.schnorrsigSign ctx sigBuf msgHashPtr keyPairPtr nullPtr
758+
ret <- case mGen of
759+
Just gen -> do
760+
let randomBytes = BS.pack $ Prelude.take 32 $ randoms gen
761+
BS.useAsCStringLen randomBytes $ \(ptr, _) ->
762+
Prim.schnorrsigSign ctx sigBuf msgHashPtr keyPairPtr (castPtr ptr)
763+
Nothing ->
764+
Prim.schnorrsigSign ctx sigBuf msgHashPtr keyPairPtr nullPtr
715765
if isSuccess ret
716-
then Just . Signature <$> newForeignPtr finalizerFree sigBuf
717-
else free sigBuf $> Nothing
766+
then Just . SchnorrSignature <$> newForeignPtr finalizerFree sigBuf
767+
else do
768+
free sigBuf
769+
return Nothing
770+
771+
772+
-- | Compute a deterministic schnorr signature using a 'KeyPair'.
773+
schnorrSignDeterministic :: KeyPair -> ByteString -> Maybe SchnorrSignature
774+
schnorrSignDeterministic = schnorrSign Nothing
775+
776+
777+
-- | Compute a non-deterministic schnorr signature using a 'KeyPair'.
778+
schnorrSignNondeterministic :: KeyPair -> ByteString -> IO (Maybe SchnorrSignature)
779+
schnorrSignNondeterministic kp bs = newStdGen >>= \gen -> pure $ schnorrSign (Just gen) kp bs
718780

719781

720782
-- | Verify the authenticity of a schnorr signature. @True@ means the 'Signature' is correct.
721-
schnorrVerify :: PubKeyXO -> ByteString -> Signature -> Bool
722-
schnorrVerify PubKeyXO{..} bs Signature{..} = unsafePerformIO . evalContT $ do
783+
schnorrVerify :: PubKeyXO -> ByteString -> SchnorrSignature -> Bool
784+
schnorrVerify PubKeyXO{..} bs SchnorrSignature{..} = unsafePerformIO . evalContT $ do
723785
pubKeyPtr <- ContT (withForeignPtr pubKeyXOFPtr)
724-
signaturePtr <- ContT (withForeignPtr signatureFPtr)
786+
schnorrSignaturePtr <- ContT (withForeignPtr schnorrSignatureFPtr)
725787
(msgPtr, msgLen) <- ContT (unsafeUseByteString bs)
726-
lift $ isSuccess <$> Prim.schnorrsigSignVerify ctx signaturePtr msgPtr msgLen pubKeyPtr
788+
lift $ isSuccess <$> Prim.schnorrsigSignVerify ctx schnorrSignaturePtr msgPtr msgLen pubKeyPtr
727789

728790

729791
-- | Generate a tagged sha256 digest as specified in BIP340

test/Crypto/Secp256k1Prop.hs

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module Crypto.Secp256k1Prop where
55

66
import Control.Applicative (Applicative (liftA2), empty)
77
import Control.Monad (when)
8+
import Control.Monad.IO.Class (liftIO)
89
import Control.Monad.Trans.Class (lift)
910
import Crypto.Secp256k1
1011
import Crypto.Secp256k1.Gen
@@ -16,6 +17,7 @@ import Hedgehog
1617
import Hedgehog.Gen hiding (discard, maybe, prune)
1718
import Hedgehog.Range (linear, singleton)
1819
import Text.Read (readMaybe)
20+
import System.Random (StdGen, mkStdGen)
1921

2022

2123
prop_secKeyReadInvertsShow :: Property
@@ -260,9 +262,17 @@ prop_schnorrSignaturesProducedAreValid = property $ do
260262
sk <- forAll secKeyGen
261263
msg <- forAll $ bytes (singleton 32)
262264
let kp = keyPairCreate sk
263-
case schnorrSign kp msg of
264-
Nothing -> failure
265-
Just sig -> assert $ schnorrVerify (fst $ keyPairPubKeyXO kp) msg sig
265+
sig <- maybe failure pure $ schnorrSignDeterministic kp msg
266+
assert $ schnorrVerify (fst $ keyPairPubKeyXO kp) msg sig
267+
268+
269+
prop_schnorrSignaturesProducedAreValidNonDeterministic :: Property
270+
prop_schnorrSignaturesProducedAreValidNonDeterministic = property $ do
271+
sk <- forAll secKeyGen
272+
msg <- forAll $ bytes (singleton 32)
273+
let kp = keyPairCreate sk
274+
sig <- liftIO $ maybe (error "Failed to sign") pure =<< schnorrSignNondeterministic kp msg
275+
assert $ schnorrVerify (fst $ keyPairPubKeyXO kp) msg sig
266276

267277

268278
prop_pubKeyCombineTweakIdentity :: Property
@@ -297,9 +307,18 @@ prop_schnorrSignaturesUnforgeable = property $ do
297307
let kp = keyPairCreate sk
298308
pk <- forAll pubKeyXOGen
299309
msg <- forAll $ bytes (singleton 32)
300-
case schnorrSign kp msg of
301-
Nothing -> failure
302-
Just sig -> assert . not $ schnorrVerify pk msg sig
310+
sig <- maybe failure pure $ schnorrSignDeterministic kp msg
311+
assert . not $ schnorrVerify pk msg sig
312+
313+
314+
prop_schnorrSignaturesUnforgeableNonDeterministic :: Property
315+
prop_schnorrSignaturesUnforgeableNonDeterministic = property $ do
316+
sk <- forAll secKeyGen
317+
let kp = keyPairCreate sk
318+
pk <- forAll pubKeyXOGen
319+
msg <- forAll $ bytes (singleton 32)
320+
sig <- liftIO $ maybe (error "Failed to sign") pure =<< schnorrSignNondeterministic kp msg
321+
assert . not $ schnorrVerify pk msg sig
303322

304323

305324
newtype Wrapped a = Wrapped {secKey :: a} deriving (Show, Read, Eq)
@@ -333,11 +352,27 @@ prop_derivedCompositeReadShowInvertTweak = derivedCompositeReadShowInvertTemplat
333352

334353

335354
prop_derivedCompositeReadShowInvertSignature :: Property
336-
prop_derivedCompositeReadShowInvertSignature = derivedCompositeReadShowInvertTemplate $ choice [ecdsa, schnorr]
355+
prop_derivedCompositeReadShowInvertSignature = derivedCompositeReadShowInvertTemplate ecdsaSignGen
337356
where
338-
base = liftA2 (,) secKeyGen (bytes (singleton 32))
339-
ecdsa = base >>= maybe empty pure . uncurry ecdsaSign
340-
schnorr = base >>= maybe empty pure . uncurry (schnorrSign . keyPairCreate)
357+
ecdsaSignGen = do
358+
sk <- secKeyGen
359+
msg <- bytes (singleton 32)
360+
maybe empty pure $ ecdsaSign sk msg
361+
362+
363+
prop_derivedCompositeReadShowInvertSchnorrSignature :: Property
364+
prop_derivedCompositeReadShowInvertSchnorrSignature = property $ do
365+
sk <- forAll secKeyGen
366+
let kp = keyPairCreate sk
367+
msg <- forAll $ bytes (singleton 32)
368+
sig <- maybe failure pure $ schnorrSignDeterministic kp msg
369+
let a = sig
370+
annotateShow a
371+
annotateShow (length $ show a)
372+
annotateShow (Wrapped a)
373+
case readMaybe (show (Wrapped a)) of
374+
Nothing -> failure
375+
Just x -> x === Wrapped a
341376

342377

343378
prop_derivedCompositeReadShowInvertRecoverableSignature :: Property
@@ -355,5 +390,61 @@ prop_eqImportImpliesEqSecKey = property $ do
355390
k0 === k1
356391

357392

393+
prop_schnorrSignatureParseInvertsSerialize :: Property
394+
prop_schnorrSignatureParseInvertsSerialize = property $ do
395+
sk <- forAll secKeyGen
396+
msg <- forAll $ bytes (singleton 32)
397+
let kp = keyPairCreate sk
398+
sig <- maybe failure pure $ schnorrSignDeterministic kp msg
399+
let serialized = exportSchnorrSignature sig
400+
annotateShow serialized
401+
annotateShow (BS.length serialized)
402+
let parsed = importSchnorrSignature serialized
403+
parsed === Just sig
404+
405+
406+
prop_schnorrSignatureValidityPreservedOverSerialization :: Property
407+
prop_schnorrSignatureValidityPreservedOverSerialization = property $ do
408+
sk <- forAll secKeyGen
409+
msg <- forAll $ bytes (singleton 32)
410+
let kp = keyPairCreate sk
411+
sig <- maybe failure pure $ schnorrSignDeterministic kp msg
412+
let serialized = exportSchnorrSignature sig
413+
let parsed = importSchnorrSignature serialized
414+
parsed === Just sig
415+
assert $ schnorrVerify (fst $ keyPairPubKeyXO kp) msg sig
416+
417+
418+
prop_schnorrSignatureDeterministic :: Property
419+
prop_schnorrSignatureDeterministic = property $ do
420+
sk <- forAll secKeyGen
421+
msg <- forAll $ bytes (singleton 32)
422+
let kp = keyPairCreate sk
423+
sig1 <- maybe failure pure $ schnorrSignDeterministic kp msg
424+
sig2 <- maybe failure pure $ schnorrSignDeterministic kp msg
425+
sig1 === sig2
426+
427+
428+
prop_schnorrSignatureNonDeterministic :: Property
429+
prop_schnorrSignatureNonDeterministic = property $ do
430+
sk <- forAll secKeyGen
431+
msg <- forAll $ bytes (singleton 32)
432+
let kp = keyPairCreate sk
433+
sig1 <- liftIO $ maybe (error "Failed to sign") pure =<< schnorrSignNondeterministic kp msg
434+
sig2 <- liftIO $ maybe (error "Failed to sign") pure =<< schnorrSignNondeterministic kp msg
435+
sig1 /== sig2
436+
437+
438+
prop_schnorrSignWithStdGen :: Property
439+
prop_schnorrSignWithStdGen = property $ do
440+
sk <- forAll secKeyGen
441+
msg <- forAll $ bytes (singleton 32)
442+
let kp = keyPairCreate sk
443+
stdGen <- forAll $ mkStdGen <$> integral (linear 0 maxBound)
444+
sig1 <- maybe failure pure $ schnorrSign (Just stdGen) kp msg
445+
sig2 <- maybe failure pure $ schnorrSign (Just stdGen) kp msg
446+
sig1 === sig2
447+
448+
358449
tests :: Group
359-
tests = $$discover
450+
tests = $$discover

0 commit comments

Comments
 (0)