Skip to content

Commit 5a1feec

Browse files
committed
- Add randomness to Schnorr signatures for BIP340 compliance.
- Updated schnorrSign to produce non-deterministic signatures using randomness. - Introduced SchnorrSignature type with parsing and serialization functions. - Updated tests and added random package dependency.
1 parent 1e4ebd2 commit 5a1feec

File tree

3 files changed

+109
-16
lines changed

3 files changed

+109
-16
lines changed

libsecp256k1.cabal

Lines changed: 1 addition & 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

src/Crypto/Secp256k1.hs

Lines changed: 56 additions & 11 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
@@ -89,7 +92,7 @@ import Crypto.Secp256k1.Internal
8992
import Crypto.Secp256k1.Prim (flagsEcUncompressed)
9093
import Crypto.Secp256k1.Prim qualified as Prim
9194
import Data.ByteArray.Encoding qualified as BA
92-
import Data.ByteArray.Sized
95+
import Data.ByteArray.Sized hiding (take)
9396
import Data.ByteString (ByteString)
9497
import Data.ByteString qualified as BS
9598
import Data.ByteString.Char8 qualified as B8
@@ -140,6 +143,7 @@ import Foreign.Storable (Storable (..))
140143
import GHC.Generics (Generic)
141144
import GHC.IO.Handle.Text (memcpy)
142145
import System.IO.Unsafe (unsafePerformIO)
146+
import System.Random (newStdGen, randoms)
143147
import Text.Read (
144148
Lexeme (String),
145149
lexP,
@@ -283,6 +287,28 @@ instance NFData Signature where
283287
rnf Signature{..} = seq signatureFPtr ()
284288

285289

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

@@ -493,6 +519,23 @@ exportRecoverableSignature RecoverableSignature{..} = unsafePerformIO . evalCont
493519
unsafePackByteString (outBuf, 65)
494520

495521

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

703746
-- | Compute a schnorr signature using a 'KeyPair'. The @ByteString@ must be 32 bytes long to get a @Just@ out of this
704747
-- function
705-
schnorrSign :: KeyPair -> ByteString -> Maybe Signature
748+
schnorrSign :: KeyPair -> ByteString -> Maybe SchnorrSignature
706749
schnorrSign KeyPair{..} bs
707750
| BS.length bs /= 32 = Nothing
708751
| otherwise = unsafePerformIO . evalContT $ do
709752
(msgHashPtr, _) <- ContT (unsafeUseByteString bs)
710753
keyPairPtr <- ContT (withForeignPtr keyPairFPtr)
711754
lift $ do
712755
sigBuf <- mallocBytes 64
713-
-- TODO: provide randomness here instead of supplying a null pointer
714-
ret <- Prim.schnorrsigSign ctx sigBuf msgHashPtr keyPairPtr nullPtr
715-
if isSuccess ret
716-
then Just . Signature <$> newForeignPtr finalizerFree sigBuf
717-
else free sigBuf $> Nothing
756+
gen <- newStdGen
757+
let randomBytes = BS.pack $ take 32 $ randoms gen
758+
BS.useAsCStringLen randomBytes $ \(randomPtr, _) -> do
759+
ret <- Prim.schnorrsigSign ctx sigBuf msgHashPtr keyPairPtr (castPtr randomPtr)
760+
if isSuccess ret
761+
then Just . SchnorrSignature <$> newForeignPtr finalizerFree sigBuf
762+
else free sigBuf $> Nothing
718763

719764

720765
-- | 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
766+
schnorrVerify :: PubKeyXO -> ByteString -> SchnorrSignature -> Bool
767+
schnorrVerify PubKeyXO{..} bs SchnorrSignature{..} = unsafePerformIO . evalContT $ do
723768
pubKeyPtr <- ContT (withForeignPtr pubKeyXOFPtr)
724-
signaturePtr <- ContT (withForeignPtr signatureFPtr)
769+
schnorrSignaturePtr <- ContT (withForeignPtr schnorrSignatureFPtr)
725770
(msgPtr, msgLen) <- ContT (unsafeUseByteString bs)
726-
lift $ isSuccess <$> Prim.schnorrsigSignVerify ctx signaturePtr msgPtr msgLen pubKeyPtr
771+
lift $ isSuccess <$> Prim.schnorrsigSignVerify ctx schnorrSignaturePtr msgPtr msgLen pubKeyPtr
727772

728773

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

test/Crypto/Secp256k1Prop.hs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ prop_signatureParseInvertsSerialize = property $ do
129129
Just x -> x === sig
130130

131131

132+
133+
134+
132135
prop_recoverableSignatureReadInvertsShow :: Property
133136
prop_recoverableSignatureReadInvertsShow = property $ do
134137
sk <- forAll secKeyGen
@@ -333,11 +336,21 @@ prop_derivedCompositeReadShowInvertTweak = derivedCompositeReadShowInvertTemplat
333336

334337

335338
prop_derivedCompositeReadShowInvertSignature :: Property
336-
prop_derivedCompositeReadShowInvertSignature = derivedCompositeReadShowInvertTemplate $ choice [ecdsa, schnorr]
339+
prop_derivedCompositeReadShowInvertSignature = derivedCompositeReadShowInvertTemplate ecdsaSignGen
340+
where
341+
ecdsaSignGen = do
342+
sk <- secKeyGen
343+
msg <- bytes (singleton 32)
344+
maybe empty pure $ ecdsaSign sk msg
345+
346+
347+
prop_derivedCompositeReadShowInvertSchnorrSignature :: Property
348+
prop_derivedCompositeReadShowInvertSchnorrSignature = derivedCompositeReadShowInvertTemplate schnorrSignGen
337349
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)
350+
schnorrSignGen = do
351+
sk <- secKeyGen
352+
msg <- bytes (singleton 32)
353+
maybe empty pure $ schnorrSign (keyPairCreate sk) msg
341354

342355

343356
prop_derivedCompositeReadShowInvertRecoverableSignature :: Property
@@ -355,5 +368,39 @@ prop_eqImportImpliesEqSecKey = property $ do
355368
k0 === k1
356369

357370

371+
prop_schnorrSignatureParseInvertsSerialize :: Property
372+
prop_schnorrSignatureParseInvertsSerialize = property $ do
373+
sk <- forAll secKeyGen
374+
msg <- forAll $ bytes (singleton 32)
375+
let kp = keyPairCreate sk
376+
sig <- maybe failure pure $ schnorrSign kp msg
377+
let serialized = exportSchnorrSignature sig
378+
annotateShow serialized
379+
annotateShow (BS.length serialized)
380+
let parsed = fromJust $ importSchnorrSignature serialized
381+
parsed === sig
382+
383+
384+
prop_schnorrSignatureValidityPreservedOverSerialization :: Property
385+
prop_schnorrSignatureValidityPreservedOverSerialization = property $ do
386+
sk <- forAll secKeyGen
387+
msg <- forAll $ bytes (singleton 32)
388+
let kp = keyPairCreate sk
389+
sig <- maybe failure pure $ schnorrSign kp msg
390+
let serialized = exportSchnorrSignature sig
391+
let parsed = fromJust $ importSchnorrSignature serialized
392+
assert $ schnorrVerify (fst $ keyPairPubKeyXO kp) msg parsed
393+
394+
395+
prop_schnorrSignatureNonDeterministic :: Property
396+
prop_schnorrSignatureNonDeterministic = property $ do
397+
sk <- forAll secKeyGen
398+
msg <- forAll $ bytes (singleton 32)
399+
let kp = keyPairCreate sk
400+
sig1 <- maybe failure pure $ schnorrSign kp msg
401+
sig2 <- maybe failure pure $ schnorrSign kp msg
402+
sig1 /== sig2
403+
404+
358405
tests :: Group
359-
tests = $$discover
406+
tests = $$discover

0 commit comments

Comments
 (0)