secp256k1-haskell: Bindings for secp256k1

[ crypto, library, mit ] [ Propose Tags ] [ Report a vulnerability ]

Sign and verify signatures using the secp256k1 library.


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
recovery

Bind the ECDSA public key recovery module of libsecp256k1. The library must have been built with it (--enable-module-recovery), as the packages of the Linux distributions, Homebrew and Nixpkgs are. Turn the flag off for a library built without it.

Enabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.1.0, 0.1.1, 0.1.2, 0.1.3, 0.1.4, 0.1.5, 0.1.6, 0.1.7, 0.1.8, 0.2.0, 0.2.1, 0.2.2, 0.2.3, 0.2.4, 0.2.5, 0.3.0, 0.3.1, 0.4.0, 0.5.0, 0.6.0, 0.6.1, 0.7.0, 1.0.0, 1.0.1, 1.1.0, 1.2.0, 1.3.0, 1.4.0, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.4.5, 1.4.6, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 2.0.0, 2.0.1
Change log CHANGELOG.md
Dependencies base (>=4.9 && <5), base16 (>=2.0), bytestring (>=0.10.8), deepseq (>=1.4.2), entropy (>=0.3.8), hashable (>=1.2.6), QuickCheck (>=2.9.2), string-conversions (>=0.4) [details]
License MIT
Author JP Rupp
Maintainer jprupp@protonmail.ch
Uploaded by jprupp at 2026-11-07T16:06:31Z
Category Crypto
Home page http://github.com/jprupp/secp256k1-haskell#readme
Bug tracker https://github.com/jprupp/secp256k1-haskell/issues
Source repo head: git clone https://github.com/jprupp/secp256k1-haskell
Distributions LTSHaskell:1.4.6, Stackage:2.0.1
Reverse Dependencies 9 direct, 21 indirect [details]
Downloads 12673 total (130 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for secp256k1-haskell-2.0.1

[back to package description]

secp256k1-haskell

Hackage CI License: MIT

Haskell bindings to libsecp256k1, the library that Bitcoin Core uses for elliptic curve cryptography on the secp256k1 curve. It covers ECDSA signatures, BIP-340 Schnorr signatures, BIP-341 Taproot tweaks, key tweaking for BIP-32 derivation, and ECDSA public key recovery for Bitcoin signed messages.

Contents

Why secp256k1-haskell

Elliptic curve cryptography is easy to write and very hard to get right. A signing routine whose running time depends on the secret key leaks it through timing. A nonce that repeats once reveals the key outright. An edge case in point arithmetic accepts a forged signature. These are not theoretical: each has cost real users real money.

libsecp256k1 is the answer the Bitcoin community converged on. It is constant-time wherever secrets are involved, blinds secret key operations against side channels, is tested exhaustively, and has been reviewed for over a decade. It is what Bitcoin Core runs.

This package lets a Haskell program use that library instead of reimplementing it, and wraps it in types that make the usual mistakes hard to make. It is the cryptographic foundation of haskoin-core and, through it, of the rest of the Haskoin libraries.

Design principles

  • Valid by construction. Every key, signature, hash and tweak goes through a smart constructor or an import function that checks it. A PubKey is always a point on the curve; a Sig always has both components in range. Invalid input is refused where it enters the program, not deep inside a computation.
  • Never abort the process. By default libsecp256k1 calls abort() on an illegal argument, which kills the whole Haskell program and cannot be caught. Every context this package creates installs a callback that makes the C function fail instead, so a bad argument becomes Nothing, False or an exception.
  • Pure where the mathematics is pure. Signing with deterministic nonces, verifying, deriving and tweaking keys are pure functions. Only operations that need fresh randomness run in IO.
  • Standard encodings at the edges. Show, Read and IsString use SEC1, compact or plain hex, never libsecp256k1's internal memory layout, which differs between platforms and library versions.
  • Explicit about Bitcoin Core. Where Bitcoin Core goes beyond the standards, such as grinding signatures to a low R value or accepting high-S signatures from old blocks, this package offers the behaviour as a separate, documented function instead of silently picking one.
  • Thin. Hashing, address encoding, BIP-32 paths and transaction formats belong in higher-level libraries such as haskoin-core.

Features

Area What you get
ECDSA Deterministic RFC 6979 signing, Bitcoin Core's low-R grinding, verification, lower-S normalization
Encodings SEC1 public keys (33 or 65 bytes), DER and compact signatures, hex Show/Read/IsString
Schnorr (BIP-340) Signing (deterministic, with given or fresh auxiliary randomness), verification, x-only keys
Taproot (BIP-341) Tweaking an x-only public key and its secret key
Key arithmetic Add or multiply secret and public keys by a tweak, negate, combine public keys
Recovery Recoverable ECDSA signatures and public key recovery, as Bitcoin signed messages use
Safety Validated values, no aborts, length checks before every C call, contexts shared across threads
Testing QuickCheck Arbitrary instances for secret keys, hashes and tweaks

What is not included

  • No hashing. Pass a 32-byte hash you computed with a hash library, such as cryptohash-sha256 or haskoin-core.
  • No ECDH, MuSig2 or adaptor signatures, although libsecp256k1 or its forks offer some of them.
  • No secure erasure of secret keys from memory. See Security notes.

Installation

System library

The package needs libsecp256k1 0.2.0 or later and pkg-config to find it. Public key recovery also needs the library's recovery module, which the packages below include.

# Debian and Ubuntu (24.04 ships 0.2.0)
sudo apt-get install libsecp256k1-dev pkg-config

# Fedora
sudo dnf install libsecp256k1-devel pkgconf

# macOS with Homebrew
brew install secp256k1 pkg-config

# Nix
nix-shell -p secp256k1 pkg-config

Check what you have:

pkg-config --modversion libsecp256k1   # must print 0.2.0 or later

Older distributions, such as Debian 11, ship an earlier version. Build the library from source then:

git clone https://github.com/bitcoin-core/secp256k1.git
cd secp256k1
./autogen.sh
./configure --enable-module-recovery
make && sudo make install

If your library was built without the recovery module, build this package with the recovery flag off: stack build --flag secp256k1-haskell:-recovery, or cabal build -f -recovery. Everything except Crypto.Secp256k1.Recovery still works.

Step by step with Stack

  1. Install Stack and the library above.

  2. Create a project:

    stack new sig-demo simple
    cd sig-demo
    
  3. Add secp256k1-haskell and bytestring to build-depends in sig-demo.cabal.

  4. If your resolver does not include the package, add it to stack.yaml:

    extra-deps:
      - secp256k1-haskell-2.0.1
      - base16-2.0
    
  5. Put the quick start program in src/Main.hs and run it with stack run. It prints True three times.

With Cabal

build-depends:
    base
  , bytestring
  , secp256k1-haskell

Run cabal update first, then cabal build.

Quick start

{-# LANGUAGE OverloadedStrings #-}

import Crypto.Secp256k1

main :: IO ()
main = do
  ctx <- createContext

  -- Hex literals go through IsString, which validates them.
  let key = "c7e5a2b4f4c5d7e8a9b0c1d2e3f405162738495a6b7c8d9e0f1a2b3c4d5e6f70" :: SecKey
      hash = "4b688df40bcedbe641ddb16ff0a1842d9c67ea1c3bf63f3e0471baa664531d1a" :: Msg
      pub = derivePubKey ctx key

  -- ECDSA
  let s = signMsg ctx key hash
  print (verifySig ctx pub s hash)

  -- BIP-340 Schnorr, with fresh auxiliary randomness
  schnorr <- signSchnorrIO ctx key hash
  print (verifySchnorr ctx pub schnorr hash)

  -- Round trip through the standard encodings
  let Just pub' = importPubKey ctx (exportPubKey ctx True pub)
  print (pub == pub')

Tutorial: sign, verify and serialize

This walks through what a wallet or a protocol implementation typically does, and why each step looks the way it does.

1. Create one context and share it

ctx <- createContext

A context holds precomputed tables and random blinding values. Creating one is relatively expensive, and one context can be used by every thread for the life of the program. createContext randomizes it once, which is what libsecp256k1 recommends.

2. Get keys from bytes

case secKey keyBytes of
  Nothing -> fail "not a valid secret key"
  Just key -> ...

secKey refuses 0 and numbers not below the curve order, so a SecKey is always usable. In practice, keys come from a wallet: from a BIP-39 mnemonic and BIP-32 derivation in haskoin-core, or from 32 bytes of the operating system's secure randomness (getEntropy 32 from the entropy package).

3. Hash, then sign

The library signs 32-byte hashes, not messages. Which hash to use is part of the protocol: Bitcoin transactions use double SHA-256 of a specific serialization, BIP-340 uses tagged hashes. Build the Msg from your hash:

let Just m = msg hashBytes
    s = signMsgLowR ctx key m -- or signMsg for plain RFC 6979

Signing is deterministic: the same key and hash give the same signature, so there is no random number generator to get wrong.

4. Serialize for the wire

Never send the get bytes of a PubKey or Sig: they are libsecp256k1's internal layout. Use the standard encodings:

let pubBytes = exportPubKey ctx True pub -- 33-byte compressed SEC1
    derBytes = exportSig ctx s -- DER, as in Bitcoin transactions
    compactBytes = exportCompactSig ctx s -- 64 bytes r || s

5. Parse and verify what you receive

verified = fromMaybe False $ do
  pub <- importPubKey ctx pubBytes
  s <- importSig ctx derBytes
  m <- msg hashBytes
  return (verifySig ctx pub (normalizeSig ctx s) m)

Every import returns Maybe, so malformed input is handled in one place. normalizeSig is there because verifySig only accepts lower-S signatures; apply it when the signature may come from software that does not produce them, such as old Bitcoin transactions. Leave it out when your protocol requires lower-S, as Bitcoin's relay rules do.

6. Move to Schnorr where the protocol allows

sig <- signSchnorrIO ctx key m
let ok = verifySchnorr ctx pub sig m
    xonly = exportXOnlyPubKey ctx pub -- 32 bytes, as BIP-340 publishes keys

Schnorr signatures are always 64 bytes, are not malleable, and are what Taproot uses. signSchnorrIO mixes fresh randomness into the nonce, as BIP-340 recommends, to protect against fault attacks.

Concepts

Values

Type What it holds Built with
SecKey 32-byte number from 1 to n − 1 secKey, IsString
PubKey A point on the curve importPubKey, derivePubKey
Msg 32-byte hash msg
Sig ECDSA signature with both components from 1 to n − 1 signMsg, importSig, importCompactSig
CompactSig 64 bytes, r ‖ s exportCompactSig, compactSig
SchnorrSig 64-byte BIP-340 signature signSchnorr, schnorrSig
Tweak 32-byte number from 0 to n − 1 tweak
RecSig ECDSA signature with its recovery id signRecMsg, importCompactRecSig

Here n is the order of the curve. The raw constructors are hidden, so these are the only ways to build values. Each type has a get field with its bytes.

Contexts

Every operation takes a Ctx. It is created once, shared freely between threads, and destroyed by the garbage collector. The only operation that modifies a context is randomizeContext, which needs exclusive access; most programs never call it.

Errors

Parsing and constructing return Maybe. Arithmetic that can produce an invalid result, such as a tweak that cancels a key, returns Maybe. Pure operations on valid values, such as signing, cannot fail and return their result directly. Programming errors, such as a value of the wrong length built with an internal constructor, raise an exception.

Usage guide

Lower-S and low-R

isLowS ctx s -- the form verifySig accepts
isLowR ctx s -- r below 2^255, so DER is at most 70 bytes
normalizeSig ctx s -- the lower-S form of any signature

Key tweaking

let Just t = tweak tweakBytes
    Just childKey = tweakAddSecKey ctx parentKey t
    Just childPub = tweakAddPubKey ctx parentPub t
-- derivePubKey ctx childKey == childPub

This is the operation behind BIP-32 derivation; the tweak itself comes from an HMAC of the chain code, computed by haskoin-core. tweakMulSecKey, tweakMulPubKey, tweakNegate and combinePubKeys cover the other group operations.

Taproot

let Just t = tweak taprootTweakBytes -- tagged hash of key and script tree
    Just outputKey = tweakAddXOnlyPubKey ctx internalKey t
    Just outputSecKey = tweakAddXOnlySecKey ctx internalSecKey t

Computing the tweak, a BIP-341 tagged hash, is left to the caller.

Bitcoin signed messages

import Crypto.Secp256k1.Recovery

let r = signRecMsg ctx key m
    (compact, recId) = exportCompactRecSig ctx r
-- the 65-byte message signature is a header byte (27 + recId, + 4 if the key
-- is compressed) followed by the 64 compact bytes
recover ctx r m -- Just the signer's PubKey

Testing with QuickCheck

SecKey, Msg and Tweak have Arbitrary instances, so properties can range over them directly. With a context ctx created once for the test suite:

prop_roundTrip :: SecKey -> Msg -> Bool
prop_roundTrip key m =
  verifySig ctx (derivePubKey ctx key) (signMsg ctx key m) m

Differences from Bitcoin Core

Topic This package Bitcoin Core
ECDSA nonce signMsg: plain RFC 6979 Grinds for low R
Low-R signatures signMsgLowR: same grinding, same signatures Default since 0.17
High-S signatures verifySig refuses them; use normalizeSig Normalizes before verifying
Verify after ECDSA signing No Yes, as a precaution
Taproot tweak when signing Separate step: tweakAddXOnlySecKey Built into Schnorr signing
Message signatures signRecMsg, no grinding Same, so signatures are identical

Module overview

Module Contents
Crypto.Secp256k1 The public API: contexts, keys, ECDSA, Schnorr, tweaks
Crypto.Secp256k1.Recovery Recoverable signatures and public key recovery
Crypto.Secp256k1.Internal.* Raw constructors and foreign imports; no stability promise

Security notes

  • Secret keys stay in memory. A SecKey is an ordinary ByteString. The garbage collector may copy it, and nothing clears it when it is no longer used. If your threat model includes an attacker reading process memory, keep keys in a hardware device instead.
  • Show prints secret keys. Showing a record that holds a SecKey, in a log line or an error message, reveals it.
  • Verification only proves what was signed. It is your protocol's job to hash the right data; signing a hash an attacker chose is as good as signing whatever they want.
  • Use fresh randomness for Schnorr when you can. signSchnorrIO protects against fault attacks that deterministic signing does not.
  • Keep libsecp256k1 up to date. Security fixes land in the C library; this package only binds it.

Development

git clone https://github.com/jprupp/secp256k1-haskell.git
cd secp256k1-haskell

stack build                        # build against the system library
stack --nix build                  # or take GHC and libsecp256k1 from nixpkgs
stack test                         # run the tests
stack haddock --no-haddock-deps    # build the API documentation

package.yaml is the source of the package description. Change it rather than secp256k1-haskell.cabal, which hpack generates.

The tests are in test/ and use hspec and QuickCheck, with published test vectors from RFC 6979, Bitcoin Core and BIP-340. CI runs them with the system library and with stack --nix.

Contributing

Pull requests are welcome.

  • Add tests for the behaviour you change, with published test vectors where they exist.
  • Keep the documentation in step with the code, as CLAUDE.md describes.
  • Describe the change in CHANGELOG.md, which follows Keep a Changelog.
  • haskoin-core builds Bitcoin keys, addresses, transactions and PSBTs on this package.
  • libsecp256k1 is the C library underneath.

License

secp256k1-haskell is released under the MIT License.