A pure Haskell implementation of BIP0340 Schnorr signatures, deterministic RFC6979 ECDSA (with BIP0146-style "low-S" signatures), ECDH, and various primitives on the elliptic curve secp256k1.
(See also ppad-csecp256k1 for FFI bindings to bitcoin-core/secp256k1.)
A sample GHCi session:
> -- pragmas and b16 import for illustration only; not required
> :set -XOverloadedStrings
> :set -XBangPatterns
> import qualified Data.ByteString.Base16 as B16
>
> -- import qualified
> import qualified Crypto.Curve.Secp256k1 as Secp256k1
>
> -- secret, public keys
> let sec = 0xB7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF
:{
ghci| let Just pub = Secp256k1.parse_point . B16.decodeLenient $
ghci| "DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659"
ghci| :}
>
> let msg = "i approve of this message"
>
> -- create and verify a schnorr signature for the message
> let sig0 = Secp256k1.sign_schnorr sec msg mempty
> Secp256k1.verify_schnorr msg pub sig0
True
>
> -- create and verify a low-S ECDSA signature for the message
> let sig1 = Secp256k1.sign_ecdsa sec msg
> Secp256k1.verify_ecdsa msg pub sig1
True
>
> -- for faster signs (especially w/ECDSA) and verifies, use a context
> let !tex = Secp256k1.precompute
> Secp256k1.verify_schnorr' tex msg pub sig0
True
Haddocks (API documentation, etc.) are hosted at docs.ppad.tech/secp256k1.
The aim is best-in-class performance for pure, highly-auditable Haskell code.
Current benchmark figures on a relatively-beefy NixOS VPS look like
(use cabal bench
to run the benchmark suite):
benchmarking schnorr/sign_schnorr'
time 2.584 ms (2.491 ms .. 2.646 ms)
0.994 R² (0.991 R² .. 0.997 R²)
mean 2.459 ms (2.426 ms .. 2.492 ms)
std dev 114.5 μs (95.32 μs .. 142.8 μs)
benchmarking schnorr/verify_schnorr'
time 1.283 ms (1.263 ms .. 1.301 ms)
0.999 R² (0.998 R² .. 0.999 R²)
mean 1.273 ms (1.260 ms .. 1.284 ms)
std dev 41.56 μs (31.12 μs .. 54.35 μs)
benchmarking ecdsa/sign_ecdsa'
time 222.6 μs (219.9 μs .. 224.9 μs)
0.999 R² (0.999 R² .. 1.000 R²)
mean 219.1 μs (217.8 μs .. 220.5 μs)
std dev 4.523 μs (3.525 μs .. 6.158 μs)
benchmarking ecdsa/verify_ecdsa'
time 1.267 ms (1.260 ms .. 1.276 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 1.278 ms (1.273 ms .. 1.286 ms)
std dev 21.32 μs (15.43 μs .. 30.82 μs)
In terms of allocations, we get:
schnorr
Case Allocated GCs
sign_schnorr' 3,273,824 0
verify_schnorr' 1,667,360 0
ecdsa
Case Allocated GCs
sign_ecdsa' 324,672 0
verify_ecdsa' 3,796,328 0
ecdh
Case Allocated GCs
ecdh (small) 2,141,736 0
ecdh (large) 2,145,464 0
This library aims at the maximum security achievable in a garbage-collected language under an optimizing compiler such as GHC, in which strict constant-timeness can be challenging to achieve.
The Schnorr implementation within has been tested against the official BIP0340 vectors, and ECDSA and ECDH have been tested against the relevant Wycheproof vectors, so their implementations are likely to be accurate and safe from attacks targeting e.g. faulty nonce generation or malicious inputs for signature or public key parameters. Timing-sensitive operations, e.g. elliptic curve scalar multiplication, have been explicitly written so as to execute algorithmically in time constant with respect to secret data, and evidence from benchmarks supports this:
benchmarking derive_pub/sk = 2
time 1.513 ms (1.468 ms .. 1.565 ms)
0.994 R² (0.991 R² .. 0.997 R²)
mean 1.579 ms (1.557 ms .. 1.600 ms)
std dev 74.25 μs (60.33 μs .. 93.80 μs)
benchmarking derive_pub/sk = 2 ^ 255 - 19
time 1.571 ms (1.530 ms .. 1.599 ms)
0.997 R² (0.995 R² .. 0.998 R²)
mean 1.574 ms (1.553 ms .. 1.589 ms)
std dev 57.72 μs (45.29 μs .. 71.48 μs)
benchmarking schnorr/sign_schnorr' (small)
time 2.436 ms (2.357 ms .. 2.516 ms)
0.995 R² (0.994 R² .. 0.998 R²)
mean 2.563 ms (2.532 ms .. 2.588 ms)
std dev 95.87 μs (71.98 μs .. 127.2 μs)
benchmarking schnorr/sign_schnorr' (large)
time 2.470 ms (2.372 ms .. 2.543 ms)
0.993 R² (0.989 R² .. 0.997 R²)
mean 2.407 ms (2.374 ms .. 2.443 ms)
std dev 123.7 μs (110.7 μs .. 144.9 μs)
benchmarking ecdsa/sign_ecdsa' (small)
time 206.9 μs (202.7 μs .. 211.8 μs)
0.997 R² (0.996 R² .. 0.999 R²)
mean 213.8 μs (211.5 μs .. 215.9 μs)
std dev 7.476 μs (5.572 μs .. 9.698 μs)
benchmarking ecdsa/sign_ecdsa' (large)
time 216.7 μs (211.6 μs .. 221.7 μs)
0.997 R² (0.995 R² .. 0.999 R²)
mean 221.8 μs (219.5 μs .. 224.1 μs)
std dev 7.673 μs (6.124 μs .. 10.69 μs)
benchmarking ecdh/ecdh (small)
time 1.623 ms (1.605 ms .. 1.639 ms)
0.999 R² (0.998 R² .. 1.000 R²)
mean 1.617 ms (1.603 ms .. 1.624 ms)
std dev 32.52 μs (20.66 μs .. 55.97 μs)
benchmarking ecdh/ecdh (large)
time 1.623 ms (1.580 ms .. 1.661 ms)
0.996 R² (0.992 R² .. 0.998 R²)
mean 1.625 ms (1.606 ms .. 1.641 ms)
std dev 58.38 μs (44.31 μs .. 78.23 μs)
Due to the use of arbitrary-precision integers, integer division modulo the elliptic curve group order does display persistent substantial timing differences on the order of 2 nanoseconds when the inputs differ dramatically in size (here 2 bits vs 255 bits):
benchmarking remQ (remainder modulo _CURVE_Q)/remQ 2
time 27.44 ns (27.19 ns .. 27.72 ns)
1.000 R² (0.999 R² .. 1.000 R²)
mean 27.23 ns (27.03 ns .. 27.43 ns)
std dev 669.1 ps (539.9 ps .. 860.2 ps)
benchmarking remQ (remainder modulo _CURVE_Q)/remQ (2 ^ 255 - 19)
time 29.11 ns (28.87 ns .. 29.33 ns)
0.999 R² (0.999 R² .. 1.000 R²)
mean 29.04 ns (28.82 ns .. 29.40 ns)
std dev 882.9 ps (647.8 ps .. 1.317 ns)
This represents the worst-case scenario (real-world private keys will never differ so extraordinarily) and is likely to be well within acceptable limits for all but the most extreme security requirements. But because we don't make "hard" guarantees of constant-time execution, take reasonable security precautions as appropriate. You shouldn't deploy the implementations within in any situation where they could easily be used as an oracle to construct a timing attack, and you shouldn't give sophisticated malicious actors access to your computer.
If you discover any vulnerabilities, please disclose them via [email protected].
You'll require Nix with flake support enabled. Enter a development shell with:
$ nix develop
Then do e.g.:
$ cabal repl ppad-secp256k1
to get a REPL for the main library.