Skip to content

ppad-tech/secp256k1

Repository files navigation

secp256k1

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.)

Usage

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 Just 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 Just 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

Documentation

Haddocks (API documentation, etc.) are hosted at docs.ppad.tech/secp256k1.

Performance

The aim is best-in-class performance for pure, highly-auditable Haskell code.

Current benchmark figures on an M4 MacBook Air look like (use cabal bench to run the benchmark suite):

  benchmarking schnorr/sign_schnorr' (large)
  time                 1.400 ms   (1.399 ms .. 1.402 ms)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 1.406 ms   (1.404 ms .. 1.408 ms)
  std dev              5.989 μs   (5.225 μs .. 7.317 μs)

  benchmarking schnorr/verify_schnorr'
  time                 720.2 μs   (716.7 μs .. 724.8 μs)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 724.6 μs   (722.0 μs .. 730.4 μs)
  std dev              12.68 μs   (6.334 μs .. 26.31 μs)

  benchmarking ecdsa/sign_ecdsa' (large)
  time                 115.3 μs   (115.1 μs .. 115.7 μs)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 116.0 μs   (115.6 μs .. 116.4 μs)
  std dev              1.367 μs   (1.039 μs .. 1.839 μs)

  benchmarking ecdsa/verify_ecdsa'
  time                 702.3 μs   (699.9 μs .. 704.9 μs)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 704.9 μs   (702.7 μs .. 708.4 μs)
  std dev              9.641 μs   (6.638 μs .. 14.04 μ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

Security

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 (with the former also being tested against noble-secp256k1's 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/wnaf, sk = 2
  time                 76.20 μs   (75.62 μs .. 77.33 μs)
                       0.999 R²   (0.998 R² .. 1.000 R²)
  mean                 75.87 μs   (75.61 μs .. 76.48 μs)
  std dev              1.218 μs   (614.3 ns .. 2.291 μs)
  variance introduced by outliers: 11% (moderately inflated)

  benchmarking derive_pub/wnaf, sk = 2 ^ 255 - 19
  time                 76.50 μs   (75.88 μs .. 77.37 μs)
                       0.999 R²   (0.998 R² .. 1.000 R²)
  mean                 76.26 μs   (75.99 μs .. 76.93 μs)
  std dev              1.317 μs   (570.7 ns .. 2.583 μs)
  variance introduced by outliers: 12% (moderately inflated)

  benchmarking schnorr/sign_schnorr' (small)
  time                 1.430 ms   (1.424 ms .. 1.438 ms)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 1.429 ms   (1.425 ms .. 1.433 ms)
  std dev              13.71 μs   (10.48 μs .. 18.85 μs)

  benchmarking schnorr/sign_schnorr' (large)
  time                 1.400 ms   (1.399 ms .. 1.402 ms)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 1.406 ms   (1.404 ms .. 1.408 ms)
  std dev              5.989 μs   (5.225 μs .. 7.317 μs)

  benchmarking ecdsa/sign_ecdsa' (small)
  time                 114.5 μs   (114.0 μs .. 115.3 μs)
                       1.000 R²   (0.999 R² .. 1.000 R²)
  mean                 115.2 μs   (114.8 μs .. 115.8 μs)
  std dev              1.650 μs   (1.338 μs .. 2.062 μs)

  benchmarking ecdsa/sign_ecdsa' (large)
  time                 115.3 μs   (115.1 μs .. 115.7 μs)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 116.0 μs   (115.6 μs .. 116.4 μs)
  std dev              1.367 μs   (1.039 μs .. 1.839 μs)

  benchmarking ecdh/ecdh (small)
  time                 907.0 μs   (902.8 μs .. 912.0 μs)
                       1.000 R²   (0.999 R² .. 1.000 R²)
  mean                 909.5 μs   (907.0 μs .. 913.0 μs)
  std dev              10.05 μs   (6.943 μs .. 17.11 μs)

  benchmarking ecdh/ecdh (large)
  time                 922.9 μs   (911.0 μs .. 937.4 μs)
                       0.999 R²   (0.998 R² .. 1.000 R²)
  mean                 915.8 μs   (911.9 μs .. 922.5 μs)
  std dev              16.84 μs   (9.830 μs .. 26.48 μ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 1-2 nanoseconds when the inputs differ dramatically in size (here 2 bits vs 255 bits):

  benchmarking remQ (remainder modulo _CURVE_Q)/remQ 2
  time                 11.13 ns   (11.12 ns .. 11.14 ns)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 11.10 ns   (11.09 ns .. 11.11 ns)
  std dev              33.75 ps   (30.27 ps .. 38.31 ps)

  benchmarking remQ (remainder modulo _CURVE_Q)/remQ (2 ^ 255 - 19)
  time                 12.50 ns   (12.49 ns .. 12.51 ns)
                       1.000 R²   (1.000 R² .. 1.000 R²)
  mean                 12.51 ns   (12.51 ns .. 12.52 ns)
  std dev              26.72 ps   (14.45 ps .. 45.87 ps)

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].

Development

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.

About

Pure Haskell Schnorr, ECDSA, ECDH on the elliptic curve secp256k1

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published