secp256k1-haskell
Bindings for secp256k1
http://github.com/jprupp/secp256k1-haskell#readme
| LTS Haskell 24.66: | 1.4.6 |
| Stackage Nightly 2026-11-08: | 2.0.1 |
| Latest on Hackage: | 2.0.1 |
secp256k1-haskell-2.0.1@sha256:bfcca05cde8d909403be31cf0f2d6a96d4072cc814f0656478edc32f8cb37d3e,2704Module documentation for 2.0.1
secp256k1-haskell
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
- Design principles
- Features
- Installation
- Quick start
- Tutorial: sign, verify and serialize
- Concepts
- Usage guide
- Differences from Bitcoin Core
- Module overview
- Security notes
- Development
- Related projects
- License
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
PubKeyis always a point on the curve; aSigalways 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 becomesNothing,Falseor 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,ReadandIsStringuse 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-sha256or 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
-
Install Stack and the library above.
-
Create a project:
stack new sig-demo simple cd sig-demo -
Add
secp256k1-haskellandbytestringtobuild-dependsinsig-demo.cabal. -
If your resolver does not include the package, add it to
stack.yaml:extra-deps: - secp256k1-haskell-2.0.1 - base16-2.0 -
Put the quick start program in
src/Main.hsand run it withstack run. It printsTruethree 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
SecKeyis an ordinaryByteString. 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. Showprints secret keys. Showing a record that holds aSecKey, 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.
signSchnorrIOprotects 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.mddescribes. - Describe the change in
CHANGELOG.md, which follows Keep a Changelog.
Related projects
- 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.
Changes
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
[2.0.1] - 2026-11-07
Changed
- Documentation: the README explains the purpose and design of the bindings, with installation steps for every platform, a tutorial, a usage guide, the differences from Bitcoin Core and security notes. Every module, including the foreign imports, has Haddock, and the main module and key functions say why they work the way they do.
- CI builds, tests and documents the package with Stack, against the
libsecp256k1 of Ubuntu and with
stack --nix. - Build with Stackage nightly-2026-11-07.
[2.0.0] - 2026-11-03
This release follows a review of the bindings, including the changes of 1.8.0. The raw constructors are hidden, the text instances use standard encodings, and more values are validated when they are built. Several changes are breaking; see Changed.
Changed
- The raw constructors of
SecKey,PubKey,Sig,CompactSig,SchnorrSig,MsgandTweak, and theCtxconstructor, are no longer exported fromCrypto.Secp256k1. Build values withsecKey,pubKey,sig,compactSig,schnorrSig,msg,tweakor the import functions. Thegetfield is still exported. The constructors remain available fromCrypto.Secp256k1.Internal.Base, without validation. tweakrejects numbers not below the curve order.sigrejects a zero component.ReadandIsStringfollow.Show,ReadandIsStringforPubKeyuse the compressed SEC1 encoding in hex;ReadandIsStringalso accept the uncompressed and hybrid encodings. The same instances forSiguse the compact encoding in hex. Before, they used the internal representation of libsecp256k1, which is not portable between platforms or library versions.normalizeSigis total:Ctx -> Sig -> Sig. It returns the lower-S form of any signature. Before, it returnedNothingfor a signature that was already in that form.importSigandimportCompactSigreturnNothingfor a signature with a zero component. libsecp256k1 parses a DER component that is not below the curve order as zero while reporting success, and such a signature can never verify.tweakNegatereturnsJustzero for a zero tweak.combinePubKeysraises an error for a raw key of the wrong length, as every other function does, instead of returningNothing.signSchnorr,signSchnorrWithAuxandsignSchnorrIOverify the signature before returning it, as BIP-340 requires, and raise an error if that fails.- The package requires libsecp256k1 0.2.0 or later through
pkg-config. The Schnorr bindings of 1.8.0 already needed that version to link.
Added
- ECDSA public key recovery, in the new module
Crypto.Secp256k1.Recovery:RecSig,recSig,signRecMsg,recover,convertRecSig,importCompactRecSigandexportCompactRecSig, bound to therecoverymodule of libsecp256k1.signRecMsggives the signatures of Bitcoin Core’s message signing, andrecoveris what its message verification does. The package flagrecovery, on by default, builds the module; turn it off for a libsecp256k1 built without--enable-module-recovery. signMsgLowRgrinds the nonce until r is below 2^255, with the same extra nonce data as Bitcoin Core, so DER signatures are at most 70 bytes and are byte-for-byte the same as Bitcoin Core’s.isLowSandisLowR.Show,ReadandIsStringinstances forCompactSig.Arbitraryinstance forTweak.- Documentation of the thread-safety rule of
randomizeContext, of whatShowprints for aSecKey, of the memory handling of secret keys, of the lower-S requirement ofverifySig, of the x-only representation of BIP-340 keys, and of the differences from Bitcoin Core.
Fixed
importPubKeyandexportPubKeywere documented as DER. The format is SEC1.tweakMulPubKeywas documented as adding a multiple of the generator.- Removed the unused
unliftio-coredependency and unused imports. The library and the tests build with-Wallwithout warnings.
[1.8.0] - 2026-11-03
Added
- BIP-340 Schnorr signatures, built on the
schnorrsigandextrakeysmodules of libsecp256k1, which it enables by default since version 0.2.0:SchnorrSig,schnorrSig,signSchnorr,signSchnorrWithAux,signSchnorrIOandverifySchnorr. No build flag is needed. importXOnlyPubKeyandexportXOnlyPubKeyconvert betweenPubKeyand 32-byte BIP-340 x-only public keys.tweakAddXOnlyPubKeyandtweakAddXOnlySecKeyadd a BIP-341 Taproot tweak to a public key and to its secret key.
Fixed
secKeyrejects zero and numbers not lower than the curve order, assecp256k1_ec_seckey_verifydoes.ReadandIsStringforSecKeyfollow.pubKeyandsigreject bytes that are not a canonical internal representation of a point on the curve or of a signature with both components below the order.ReadandIsStringfollow.- An illegal argument no longer aborts the program. Contexts get a callback
that makes libsecp256k1 return failure, so a zero
PubKeybuilt with the raw constructor givesFalse,Nothingor an exception. - Functions raise an error for a
SecKey,PubKey,Sig,CompactSig,MsgorTweakof the wrong length built with the raw constructor, instead of letting libsecp256k1 read past the end of the buffer. combinePubKeyspasses the key count assize_t, as the C function expects, and returnsNothingwhen a key is not a valid point, sincesecp256k1_ec_pubkey_combinedoes not check.- Tweak functions write to a fresh buffer instead of mutating a
ByteStringin place.
[1.7.0] - 2026-10-31
Added
- Reinstaded some functions removed by accident.
[1.6.0] - 2026-10-31
Removed
- No longer does this module expose the destroyContext function. Context is now only destroyed safely by the garbage collector.
[1.5.1] - 2026-10-31
Fixed
- This package is now compatible with base16 version 2.0.
[1.5.0] - 2026-08-12
Fixed
- Use seckey instead of privkey aliases.
[1.4.6] - 2025-05-09
Removed
- Removed more unnecessary file boilerplate.
[1.4.5] - 2025-05-09
Removed
- Removed unnecessary file boilerplate.
[1.4.4] - 2025-05-09
Changed
- Simplify author name.
[1.4.3] - 2025-05-08
Changed
- New home in github.com/jprupp rather than github.com/haskoin.
[1.4.2] - 2024-12-10
Changed
- Relax all upper-bound version restrictions in
package.yaml.
[1.4.1] - 2024-12-10
Changed
- Relax version restriction on QuickCheck.
[1.4.0] - 2024-08-28
Removed
- Removed BIP-340 support. Fedora’s version of secp256k1 libraries do not have required symbols.
[1.3.0] - 2024-08-28
Added
- Added BIP-340 support.
[1.2.0] - 2023-03-14
Changed
- Make compatible with latest upstream LTS Haskell.
[1.1.0] - 2023-11-01
Changed
- Use ForeignPtr to allow the garbage collector manage the context object (Ctx).
[1.0.1] - 2023-09-20
Changed
- Reworked the structure of the Internal modules to allow having add-on packages for optional libsecp256k1 features.
[1.0.0] - 2023-07-28
Changed
- Context data structure must be created and passed explicitly.
- Field selectors are now short and duplicates are allowed.
- Use DuplicateRecordFields and OverloadedRecordDot language extensions.
[0.6.1] - 2022-04-18
Changed
- Bump version and LTS Haskell.
- Update dependencies.
[0.6.0] - 2021-08-27
Changed
- Remove all use of CPP.
- Depend on base16 instead of the old base16-bytestring package.
- Target ghc-9.0.1 compiler.
Fixed
- Do not crash on bytestrings that are backed by null C pointers.
[0.5.0] - 2020-10-16
Changed
- Include version boundaries and other changes submitted by Emily Pillmore.
[0.4.0] - 2020-07-23
Changed
- Remove fragile ForeignPtr implementation in favor of just storing ByteStrings.
- Reuse memory instead of copying when possible.
[0.3.1] - 2020-07-03
Fixed
- Use unsafe calls in FFI.
[0.3.0] - 2020-06-13
Fixed
- Compiles with all flags now.
Added
- Script to compile with all flags.
Removed
- Remove ECDH support.
- Remove Schnorr support.
- Remove Recovery support.
[0.2.5] - 2020-06-13
Changed
- Reuse context aggressively.
- Generate context in a single thread.
Fixed
- Memory deallocation bug.
[0.2.4] - 2020-06-12
Changed
- Update Cabal and package version.
[0.2.3] - 2020-06-07
Changed
- Return meaningful error upon encountering weird ret status from upstream code.
Added
- Test parallel signature creation and verification.
[0.2.2] - 2020-04-14
Removed
- Hide tweak negation behind a flag for compatibilidy with Debian 9.
Fixed
- Correct code that was not compiling with some flags enabled.
[0.2.1] - 2020-04-10
Changed
- Do not depend on hardcoded DER signatures in tests.
[0.2.0] - 2020-04-10
Added
- Support for ECDH APIs.
- Support for Schnorr APIs.
Removed
- Enabling key recovery APIs need a flag.
[0.1.8] - 2020-01-15
Added
- Add missing
NFDatainstances for some types.
[0.1.7] - 2020-01-15
Added
- Add
NFDatainstances for all types.
[0.1.6] - 2019-12-26
Added
- Use
pkgconfigfor C library dependency.
[0.1.5] - 2019-09-19
Added
- Flag for ECDH bindings.
[0.1.4] - 2018-10-25
Changed
- Constrain imports to avoid clashes with a QuickCheck function.
[0.1.3] - 2018-10-13
Added
- Hashable instances for various types.
[0.1.2] - 2018-09-10
Changed
- Separate dependencies between library and tests.
- Remove
hspecdefault to prevent problems with Nix.
Removed
- Dependency to
cryptohashnot needed.
[0.1.1] - 2018-09-10
Changed
- Update changelog to reflect name and version change.
- Update to LTS Haskell 12.9.
[0.1.0] - 2018-09-10
Changed
- Name of package change from
secp256k1tosecp256k1-haskellto avoid Nix conflicts.