From 9fff57244200d0d64d40532482b037d845cbca38 Mon Sep 17 00:00:00 2001 From: sashabeton Date: Fri, 20 Feb 2026 00:25:28 +0100 Subject: [PATCH] crypto,crypto/secp256k1: Support custom nonce counter for specialized signing --- .../libsecp256k1/include/secp256k1_recovery.h | 3 +- .../src/modules/recovery/main_impl.h | 4 +-- crypto/secp256k1/libsecp256k1/src/secp256k1.c | 6 ++-- crypto/secp256k1/secp256.go | 10 +++++- crypto/secp256k1/secp256_test.go | 34 +++++++++++++++++++ crypto/signature_cgo.go | 10 +++++- 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/crypto/secp256k1/libsecp256k1/include/secp256k1_recovery.h b/crypto/secp256k1/libsecp256k1/include/secp256k1_recovery.h index 93a2e4ccbd..34314266c5 100644 --- a/crypto/secp256k1/libsecp256k1/include/secp256k1_recovery.h +++ b/crypto/secp256k1/libsecp256k1/include/secp256k1_recovery.h @@ -87,7 +87,8 @@ SECP256K1_API int secp256k1_ecdsa_sign_recoverable( const unsigned char *msghash32, const unsigned char *seckey, secp256k1_nonce_function noncefp, - const void *ndata + const void *ndata, + const unsigned int initialcount ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); /** Recover an ECDSA public key from a signature. diff --git a/crypto/secp256k1/libsecp256k1/src/modules/recovery/main_impl.h b/crypto/secp256k1/libsecp256k1/src/modules/recovery/main_impl.h index 76a005e017..27a7ab235a 100644 --- a/crypto/secp256k1/libsecp256k1/src/modules/recovery/main_impl.h +++ b/crypto/secp256k1/libsecp256k1/src/modules/recovery/main_impl.h @@ -120,7 +120,7 @@ static int secp256k1_ecdsa_sig_recover(const secp256k1_scalar *sigr, const secp2 return !secp256k1_gej_is_infinity(&qj); } -int secp256k1_ecdsa_sign_recoverable(const secp256k1_context* ctx, secp256k1_ecdsa_recoverable_signature *signature, const unsigned char *msghash32, const unsigned char *seckey, secp256k1_nonce_function noncefp, const void* noncedata) { +int secp256k1_ecdsa_sign_recoverable(const secp256k1_context* ctx, secp256k1_ecdsa_recoverable_signature *signature, const unsigned char *msghash32, const unsigned char *seckey, secp256k1_nonce_function noncefp, const void* noncedata, const unsigned int initialcount) { secp256k1_scalar r, s; int ret, recid; VERIFY_CHECK(ctx != NULL); @@ -129,7 +129,7 @@ int secp256k1_ecdsa_sign_recoverable(const secp256k1_context* ctx, secp256k1_ecd ARG_CHECK(signature != NULL); ARG_CHECK(seckey != NULL); - ret = secp256k1_ecdsa_sign_inner(ctx, &r, &s, &recid, msghash32, seckey, noncefp, noncedata); + ret = secp256k1_ecdsa_sign_inner(ctx, &r, &s, &recid, msghash32, seckey, noncefp, noncedata, initialcount); secp256k1_ecdsa_recoverable_signature_save(signature, &r, &s, recid); return ret; } diff --git a/crypto/secp256k1/libsecp256k1/src/secp256k1.c b/crypto/secp256k1/libsecp256k1/src/secp256k1.c index a248519dfd..079e5e9e64 100644 --- a/crypto/secp256k1/libsecp256k1/src/secp256k1.c +++ b/crypto/secp256k1/libsecp256k1/src/secp256k1.c @@ -507,12 +507,12 @@ static int nonce_function_rfc6979(unsigned char *nonce32, const unsigned char *m const secp256k1_nonce_function secp256k1_nonce_function_rfc6979 = nonce_function_rfc6979; const secp256k1_nonce_function secp256k1_nonce_function_default = nonce_function_rfc6979; -static int secp256k1_ecdsa_sign_inner(const secp256k1_context* ctx, secp256k1_scalar* r, secp256k1_scalar* s, int* recid, const unsigned char *msg32, const unsigned char *seckey, secp256k1_nonce_function noncefp, const void* noncedata) { +static int secp256k1_ecdsa_sign_inner(const secp256k1_context* ctx, secp256k1_scalar* r, secp256k1_scalar* s, int* recid, const unsigned char *msg32, const unsigned char *seckey, secp256k1_nonce_function noncefp, const void* noncedata, const unsigned int initialcount) { secp256k1_scalar sec, non, msg; int ret = 0; int is_sec_valid; unsigned char nonce32[32]; - unsigned int count = 0; + unsigned int count = initialcount; /* Default initialization here is important so we won't pass uninit values to the cmov in the end */ *r = secp256k1_scalar_zero; *s = secp256k1_scalar_zero; @@ -572,7 +572,7 @@ int secp256k1_ecdsa_sign(const secp256k1_context* ctx, secp256k1_ecdsa_signature ARG_CHECK(signature != NULL); ARG_CHECK(seckey != NULL); - ret = secp256k1_ecdsa_sign_inner(ctx, &r, &s, NULL, msghash32, seckey, noncefp, noncedata); + ret = secp256k1_ecdsa_sign_inner(ctx, &r, &s, NULL, msghash32, seckey, noncefp, noncedata, 0); secp256k1_ecdsa_signature_save(signature, &r, &s); return ret; } diff --git a/crypto/secp256k1/secp256.go b/crypto/secp256k1/secp256.go index 60ae7d9e55..471d3acaac 100644 --- a/crypto/secp256k1/secp256.go +++ b/crypto/secp256k1/secp256.go @@ -60,6 +60,14 @@ var ( // directly by an attacker. It is usually preferable to use a cryptographic // hash function on any input before handing it to this function. func Sign(msg []byte, seckey []byte) ([]byte, error) { + return SignUnsafe(msg, seckey, 0) +} + +// SignUnsafe signs a hash with a custom starting counter for nonce derivation. +// WARNING: This function is unsafe and should only be used for specialized cases +// like finding low-R signatures or canonical signatures. Improper use can lead +// to private key exposure. Use Sign for standard signing operations. +func SignUnsafe(msg []byte, seckey []byte, counter uint) ([]byte, error) { if len(msg) != 32 { return nil, ErrInvalidMsgLen } @@ -76,7 +84,7 @@ func Sign(msg []byte, seckey []byte) ([]byte, error) { noncefunc = C.secp256k1_nonce_function_rfc6979 sigstruct C.secp256k1_ecdsa_recoverable_signature ) - if C.secp256k1_ecdsa_sign_recoverable(context, &sigstruct, msgdata, seckeydata, noncefunc, nil) == 0 { + if C.secp256k1_ecdsa_sign_recoverable(context, &sigstruct, msgdata, seckeydata, noncefunc, nil, (C.uint)(counter)) == 0 { return nil, ErrSignFailed } diff --git a/crypto/secp256k1/secp256_test.go b/crypto/secp256k1/secp256_test.go index c7485bca08..d47d17b4a7 100644 --- a/crypto/secp256k1/secp256_test.go +++ b/crypto/secp256k1/secp256_test.go @@ -130,6 +130,40 @@ func TestSignDeterministic(t *testing.T) { } } +func TestSignUnsafe(t *testing.T) { + pubkey1, seckey := generateKeyPair() + msg := make([]byte, 32) + + sig1, err := Sign(msg, seckey) + if err != nil { + t.Fatal(err) + } + sig2, err := SignUnsafe(msg, seckey, 0) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(sig1, sig2) { + t.Fatal("signatures not equal") + } + + sig3, err := SignUnsafe(msg, seckey, 1000) + if err != nil { + t.Fatal(err) + } + + if bytes.Equal(sig1, sig3) { + t.Fatal("signatures with different count param equal") + } + + pubkey2, err := RecoverPubkey(msg, sig3) + if err != nil { + t.Errorf("recover error: %s", err) + } + if !bytes.Equal(pubkey1, pubkey2) { + t.Errorf("pubkey mismatch: want: %x have: %x", pubkey1, pubkey2) + } +} + func TestRandomMessagesWithSameKey(t *testing.T) { pubkey, seckey := generateKeyPair() keys := func() ([]byte, []byte) { diff --git a/crypto/signature_cgo.go b/crypto/signature_cgo.go index 18b78f4aac..30008dd0ad 100644 --- a/crypto/signature_cgo.go +++ b/crypto/signature_cgo.go @@ -51,12 +51,20 @@ func SigToPub(hash, sig []byte) (*ecdsa.PublicKey, error) { // // The produced signature is in the [R || S || V] format where V is 0 or 1. func Sign(digestHash []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) { + return SignUnsafe(digestHash, prv, 0) +} + +// SignUnsafe signs a hash with a custom starting counter for nonce derivation. +// WARNING: This function is unsafe and should only be used for specialized cases +// like finding low-R signatures or canonical signatures. Improper use can lead +// to private key exposure. Use Sign for standard signing operations. +func SignUnsafe(digestHash []byte, prv *ecdsa.PrivateKey, counter uint) (sig []byte, err error) { if len(digestHash) != DigestLength { return nil, fmt.Errorf("hash is required to be exactly %d bytes (%d)", DigestLength, len(digestHash)) } seckey := math.PaddedBigBytes(prv.D, prv.Params().BitSize/8) defer zeroBytes(seckey) - return secp256k1.Sign(digestHash, seckey) + return secp256k1.SignUnsafe(digestHash, seckey, counter) } // VerifySignature checks that the given public key created signature over digest.