ECDSA Utilities
Canonical low-S form, malleability defence, fixed-width signature codecs, and RFC 6979-style deterministic signing on top of the standard library's crypto/ecdsa.
The ecdsa package is a thin layer of utilities over the standard library. It does not define a key
type, a curve, or a signature struct — it operates on *ecdsa.PrivateKey, *ecdsa.PublicKey,
elliptic.Curve, and raw *big.Int pairs from crypto/ecdsa. Two problems are solved here that
the standard library leaves to you:
- Malleability. ECDSA signatures are not unique per message. Two different byte strings verify equally well, so signature bytes cannot be used as an identifier.
- Nonce dependence.
ecdsa.Signneeds entropy at signing time, and a bad or repeated nonce leaks the private key outright.
Reach for this package when you store, index, deduplicate, or compare ECDSA signatures, or when you
need signing to be reproducible on a device you do not trust to have a good RNG. Do not reach
for it for ordinary sign-and-verify: crypto/ecdsa already does that, correctly and with a
constant-time implementation. Everything here is math/big arithmetic and makes no constant-time
claim.
import "github.com/sonr-io/crypto/ecdsa"
Malleability, and why canonical form matters
An ECDSA signature is a pair (r, s) over a curve of prime order N. Verification checks a
relation that is symmetric in the sign of s:
Anyone who observes a valid signature can therefore produce a second, different, equally valid signature for the same message and the same key — without knowing the private key. The consequences are practical, not theoretical:
- Signature bytes are not an identifier. Keying a database, a replay-protection cache, or a transaction ID on raw signature bytes lets an attacker create an unbounded number of distinct entries for one authorised action. This is the Bitcoin transaction-malleability bug.
- Byte equality is not signature equality.
bytes.Equal(sigA, sigB) == falsedoes not mean two parties signed different things.
The fix everybody converged on is a canonical form: of the two valid s values, always use the
smaller one, s <= N/2. This package calls that “canonical” and provides both the coercion and the
strict rejection.
IsCanonical(s, N *big.Int)?bool
True when s <= N/2. Does not range-check s, and returns false for nil inputs.
boolMakeCanonical(r, s, N *big.Int)?(*big.Int, *big.Int)
Returns (r, min(s, N-s)). No validation and no error. Returns its inputs unchanged if any is nil.
(*big.Int, *big.Int)IsSignatureCanonical(r, s *big.Int, curve elliptic.Curve)?bool
Full check: r in [1, N-1] AND s in [1, N/2]. False on any nil argument.
boolCanonicalizeSignature(r, s, curve)?(*big.Int, *big.Int, error)
Range-checks both scalars, then returns copies with s reduced to canonical form. Errors on nil arguments or out-of-range r or s.
(*big.Int, *big.Int, error)NormalizeSignature(r, s, curve)?(*big.Int, *big.Int, error)
Currently a direct pass-through to CanonicalizeSignature. The name suggests more; the body does not do more.
(*big.Int, *big.Int, error)RejectNonCanonical(r, s, curve)?error
Strict mode: returns an error instead of coercing. Use this on ingress when you want to refuse malleated signatures outright.
errorValidateAndCanonicalizeSignature(pub, hash, r, s)?(*big.Int, *big.Int, error)
Canonicalizes, then verifies against pub and hash. Falls back to verifying the original pair if the canonical one fails. Errors if neither verifies.
(*big.Int, *big.Int, error)CompareSignatures(r1, s1, r2, s2, curve)?(bool, error)
Canonicalizes both pairs and compares. This is the correct way to ask whether two signatures are the same signature.
(bool, error)MakeCanonical and IsCanonical take a bare *big.Int order rather than a curve, which makes them
usable with secp256k1 or any other order you have on hand; the rest take an elliptic.Curve.
Choosing between coerce and reject
CanonicalizeSignature accepts a malleated signature and quietly normalises it. Right for a
verifier that must interoperate with signers you do not control, and for anything you are about
to store or hash.
RejectNonCanonical refuses. Right for a consensus rule or a protocol where you have declared
that only canonical signatures are well-formed — coercion there would let two encodings of the
same intent both be “accepted”, which is exactly the ambiguity you set out to remove.
Grounded in TestCanonicalizeSignature and TestIsSignatureCanonical.
package main
import (
stdecdsa "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"math/big"
"github.com/sonr-io/crypto/ecdsa"
)
func main() {
curve := elliptic.P256()
priv, err := stdecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Fatal(err)
}
digest := sha256.Sum256([]byte("transfer 100 to bob"))
r, s, err := stdecdsa.Sign(rand.Reader, priv, digest[:])
if err != nil {
log.Fatal(err)
}
// stdlib Sign does not normalise, so first pin down which of the pair is low-S.
N := curve.Params().N
rLow, sLow, err := ecdsa.CanonicalizeSignature(r, s, curve)
if err != nil {
log.Fatal(err)
}
// Anyone can produce this second, equally valid, non-canonical signature.
sHigh := new(big.Int).Sub(N, sLow)
fmt.Println("high-S still verifies:",
stdecdsa.Verify(&priv.PublicKey, digest[:], rLow, sHigh)) // true
// Both collapse to the same canonical pair...
same, err := ecdsa.CompareSignatures(rLow, sLow, rLow, sHigh, curve)
if err != nil {
log.Fatal(err)
}
fmt.Println("same signature:", same) // true
// ...and to the same fixed-width encoding.
a, err := ecdsa.SignatureBytes(rLow, sLow, curve)
if err != nil {
log.Fatal(err)
}
b, err := ecdsa.SignatureBytes(rLow, sHigh, curve)
if err != nil {
log.Fatal(err)
}
fmt.Println("identical bytes:", string(a) == string(b), len(a)) // true 64
// Strict ingress: refuse rather than repair.
fmt.Println("high-S accepted:", ecdsa.IsSignatureCanonical(rLow, sHigh, curve)) // false
if err := ecdsa.RejectNonCanonical(rLow, sHigh, curve); err != nil {
fmt.Println("rejected:", err) // signature is not in canonical form
}
}
Fixed-width codecs
SignatureBytes and SignatureFromBytes are a canonical, length-prefixed-free alternative to ASN.1
DER. The layout is the concatenation of two big-endian, zero-padded scalars:
| Field | Offset | Length |
|---|---|---|
r |
0 |
byteSize |
s |
byteSize |
byteSize |
where byteSize = (curve.Params().BitSize + 7) / 8. For P-256 that is 32, so a signature is exactly
64 bytes; P-384 gives 96, P-521 gives 132.
raw, err := ecdsa.SignatureBytes(r, s, curve) // canonicalizes, then encodes
r2, s2, err := ecdsa.SignatureFromBytes(raw, curve) // decodes, then canonicalizes
Both directions canonicalize, which is what makes the encoding a stable identifier: (r, s) and
(r, N-s) produce byte-identical output, and a decode always yields a canonical pair.
Deterministic signing
DeterministicSign removes the randomness from ECDSA signing. Instead of drawing k from an RNG,
it derives k from the private key and the message digest through an HMAC-DRBG construction in the
style of RFC 6979, using HMAC-SHA-256 as the
fixed underlying primitive.
DeterministicSign(priv *ecdsa.PrivateKey, hash []byte)?(*big.Int, *big.Int, error)
Derives k deterministically, signs, and returns an already-canonical (low-S) pair. Errors on a nil key, a nil D, or an empty hash.
(*big.Int, *big.Int, error)VerifyDeterministic(pub *ecdsa.PublicKey, hash []byte, r, s *big.Int)?bool
Range-checks r in [1, N-1] and s in [1, N/2], then delegates to crypto/ecdsa.Verify. Rejects a high-S signature that stdlib Verify would accept.
boolWhy determinism is worth having:
- No entropy dependence at signing time. An embedded device, a freshly-booted VM, or a deterministic test environment can sign correctly without a seeded CSPRNG.
- Reproducibility. The same key and message always yield the same signature, so signatures can be regenerated, diffed, and used as cache keys.
- No silent RNG failure. A subtly broken RNG produces biased nonces, and nonce bias leaks the private key over enough signatures. Removing the RNG removes that failure mode.
Grounded in TestDeterministicSign and TestCanonicalSignature.
package main
import (
stdecdsa "crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"log"
"github.com/sonr-io/crypto/ecdsa"
)
func main() {
priv, err := stdecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
digest := sha256.Sum256([]byte("test message for deterministic signing"))
r1, s1, err := ecdsa.DeterministicSign(priv, digest[:])
if err != nil {
log.Fatal(err)
}
r2, s2, err := ecdsa.DeterministicSign(priv, digest[:])
if err != nil {
log.Fatal(err)
}
fmt.Println("reproducible:", r1.Cmp(r2) == 0 && s1.Cmp(s2) == 0) // true
// Output is already low-S.
fmt.Println("canonical:", ecdsa.IsCanonical(s1, priv.Curve.Params().N)) // true
// Verifies with the standard library, and with the strict wrapper.
fmt.Println("stdlib ok:", stdecdsa.Verify(&priv.PublicKey, digest[:], r1, s1))
fmt.Println("strict ok:", ecdsa.VerifyDeterministic(&priv.PublicKey, digest[:], r1, s1))
}
The output is normalised to low-S inside signWithK before it is returned, so you never need to
call MakeCanonical on a DeterministicSign result.
Related
Threshold ECDSA
Produce an ECDSA signature from key shares that never combine. The canonicalization helpers here apply to its output too.
MPC enclave
The two-party ECDSA wrapper this library ships as its headline API.
Curve abstraction
The library’s own curve types — distinct from the crypto/elliptic types this package uses.
Security notes
Constant-time gaps and standards deviations across the library.