csecp256k1

secp256k1 bindings.
Log | Files | Refs | README | LICENSE

commit c7212007028054431bd1514717d415d50825910a
parent 0fd70458959d9000ff66e9607c94d6410813ee56
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 26 Feb 2024 11:47:02 +0400

lib: add module docs

Also changes the argument order on 'verify_schnorr', which is different
to that of the underlying library, but more in line with the existing
order in ECDSA 'verify'.

Diffstat:
Mlib/Crypto/Secp256k1.hs | 150++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 138 insertions(+), 12 deletions(-)

diff --git a/lib/Crypto/Secp256k1.hs b/lib/Crypto/Secp256k1.hs @@ -1,6 +1,22 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} +-- | +-- Module: Crypto.Secp256k1 +-- Copyright: (c) 2024 Jared Tobin +-- License: MIT +-- +-- Maintainer: Jared Tobin <jared@ppad.tech> +-- Stability: stable +-- Portability: portable +-- +-- Bindings to bitcoin-core/secp256k1, which "provides digital +-- signatures and other cryptographic primitives on the secp256k1 +-- elliptic curve." +-- +-- This library exposes a minimal subset of functionality, primarily +-- supporting ECDSA/Schnorr signatures and ECDH secret computation. + module Crypto.Secp256k1 ( -- exceptions Secp256k1Exception(..) @@ -53,35 +69,86 @@ import Foreign.Ptr (Ptr) import qualified Foreign.Ptr as F (castPtr, nullPtr) import qualified Foreign.Storable as S (poke, peek) --- | A secp256k1 context. +-- | A bitcoin-core/secp256k1 context. +-- +-- bitcoin-core/secp256k1 computations typically require a context, +-- the primary purpose of which is to store randomization data as +-- increased protection against side-channel attacks (and the second +-- of which is boring pointer storage to various library callbacks). +-- +-- You should create and use values of this type via 'wrcontext' or +-- 'wcontext'. newtype Context = Context (Ptr I.Context) --- | A secp256k1-internal public key. +instance Show Context where + show (Context tex) = "<bitcoin-core/secp256k1 context " <> show tex <> ">" + +-- | A bitcoin-core/secp256k1-internal public key. +-- +-- Create a value of this type by parsing a compressed or uncompressed +-- public key via 'parse_pub', deriving one from a secret key via +-- 'create_pub', or extracting one from a keypair via 'keypair_pub'. newtype Pub = Pub BS.ByteString --- | A secp256k1-internal x-only public key. +instance Show Pub where + show _ = "<bitcoin-core/secp256k1 public key>" + +-- | A bitcoin-core/secp256k1-internal x-only public key. +-- +-- An "x-only" public key corresponds to a public key with even +-- y-coordinate. +-- +-- Create a value of this type from a 'Pub' via 'xonly', or parse one +-- directly via 'parse_xonly'. newtype XOnlyPub = XOnlyPub BS.ByteString --- | A secp256k1-internal public/secret keypair. +instance Show XOnlyPub where + show _ = "<bitcoin-core/secp256k1 x-only public key>" + +-- | A bitcoin-core/secp256k1-internal public/secret keypair. +-- +-- Create a value of this type by passing a secret key to +-- 'create_keypair'. newtype KeyPair = KeyPair BS.ByteString --- | A secp256k1-internal ECDSA signature. +instance Show KeyPair where + show _ = "<bitcoin-core/secp256k1 keypair>" + +-- | A bitcoin-core/secp256k1-internal ECDSA signature. +-- +-- Create a value of this type via 'sign', or parse a DER-encoded +-- signature via 'parse_der'. newtype Sig = Sig BS.ByteString +instance Show Sig where + show _ = "<bitcoin-core/secp256k1 signature>" + -- exceptions +-- | A catch-all exception type. +-- +-- Internal library errors (i.e., non-unit return values in the +-- underlying C functions) will typically throw a Secp256k1Error +-- exception. data Secp256k1Exception = Secp256k1Error | InsufficientEntropy - | Bip340Error deriving Show instance Exception Secp256k1Exception -- context --- | Execute the supplied continuation within a fresh secp256k1 context. --- The context will be destroyed afterwards. +-- | Execute the supplied continuation within a fresh +-- bitcoin-core/secp256k1 context. The context will be destroyed +-- afterwards. +-- +-- This function executes the supplied continuation in a context +-- that has /not/ been randomized, and so /doesn't/ offer additional +-- side-channel attack protection. For that, use 'wrcontext'. +-- +-- >>> wcontext $ \tex -> parse_pub tex bytestring +-- "<bitcoin-core/secp256k1 public key>" wcontext :: (Context -> IO a) -> IO a wcontext = bracket create destroy where create = do @@ -91,11 +158,15 @@ wcontext = bracket create destroy where destroy (Context tex) = secp256k1_context_destroy tex --- | Same as 'wcontext', but randomize the secp256k1 context via the --- provided entropy before executing the supplied continuation. +-- | Same as 'wcontext', but randomize the bitcoin-core/secp256k1 +-- context with the provided entropy before executing the supplied +-- continuation. -- -- You must supply at least 32 bytes of entropy; any less will result -- in an InsufficientEntropy exception. +-- +-- >>> wrcontext entropy $ \tex -> sign tex sec msg +-- "<bitcoin-core/secp256k1 signature>" wrcontext :: BS.ByteString -> (Context -> IO a) -> IO a wrcontext enn con | BS.length enn < 32 = throwIO InsufficientEntropy @@ -116,6 +187,9 @@ wrcontext enn con -- | Derive a public key from a 32-byte secret key. -- -- The size of the input is not checked. +-- +-- >>> wrcontext entropy $ \tex -> derive_pub tex sec +-- "<bitcoin-core/secp256k1 public key>" derive_pub :: Context -> BS.ByteString -> IO Pub derive_pub (Context tex) bs = BS.useAsCString bs $ \(F.castPtr -> sec) -> @@ -129,6 +203,9 @@ derive_pub (Context tex) bs = -- | Parse a compressed (33-byte) or uncompressed (65-byte) public key. -- -- The size of the input is not checked. +-- +-- >>> wcontext $ \tex -> parse_pub tex bs +-- "<bitcoin-core/secp256k1 public key>" parse_pub :: Context -> BS.ByteString -> IO Pub parse_pub (Context tex) bs = BS.useAsCStringLen bs $ \(F.castPtr -> pub, fromIntegral -> len) -> @@ -145,11 +222,15 @@ data PubFormat = -- | Serialize a public key into a compressed (33-byte) bytestring -- representation. +-- +-- >>> wcontext $ \tex -> serialize_pub tex pub serialize_pub :: Context -> Pub -> IO BS.ByteString serialize_pub = serialize_pub_in Compressed -- | Serialize a public key into an uncompressed (65-byte) bytestring -- represention. +-- +-- >>> wcontext $ \tex -> serialize_pub_u tex pub serialize_pub_u :: Context -> Pub -> IO BS.ByteString serialize_pub_u = serialize_pub_in Uncompressed @@ -180,6 +261,9 @@ serialize_pub_in for (Context tex) (Pub pub) = -- | Sign a 32-byte message hash with the provided secret key. -- -- The sizes of the inputs are not checked. +-- +-- >>> wrcontext entropy $ \tex -> sign tex sec msg +-- "<bitcoin-core/secp256k1 signature>" sign :: Context -> BS.ByteString -> BS.ByteString -> IO Sig sign (Context tex) key msg = A.allocaBytes _SIG_BYTES $ \out -> @@ -197,6 +281,11 @@ sign (Context tex) key msg = -- Returns 'True' for a verifying signature, 'False' otherwise. -- -- The size of the input is not checked. +-- +-- >>> wcontext $ \tex -> verify tex pub msg good_sig +-- True +-- >>> wcontext $ \tex -> verify tex pub msg bad_sig +-- False verify :: Context -> Pub -> BS.ByteString -> Sig -> IO Bool verify (Context tex) (Pub pub) msg (Sig sig) = BS.useAsCString pub $ \(F.castPtr -> key) -> @@ -206,6 +295,11 @@ verify (Context tex) (Pub pub) msg (Sig sig) = pure (suc == 1) -- | Parse a DER-encoded bytestring into a signature. +-- +-- >>> wcontext $ \tex -> parse_der tex bytestring +-- "<bitcoin-core/secp256k1 signature>" +-- >>> wcontext $ \tex -> parse_der tex bad_bytestring +-- *** Exception: Secp256k1Error parse_der :: Context -> BS.ByteString -> IO Sig parse_der (Context tex) bs = BS.useAsCStringLen bs $ \(F.castPtr -> der, fromIntegral -> len) -> @@ -217,6 +311,8 @@ parse_der (Context tex) bs = pure (Sig sig) -- | Serialize a signature into a DER-encoded bytestring. +-- +-- >>> wcontext $ \tex -> serialize_der tex sig serialize_der :: Context -> Sig -> IO BS.ByteString serialize_der (Context tex) (Sig sig) = A.alloca $ \len -> @@ -235,6 +331,9 @@ serialize_der (Context tex) (Sig sig) = -- | Convert a public key into an x-only public key (i.e. one with even -- y coordinate). +-- +-- >>> wcontext $ \tex -> xonly tex pub +-- "<bitcoin-core/secp256k1 x-only public key>" xonly :: Context -> Pub -> IO XOnlyPub xonly (Context tex) (Pub pub) = A.allocaBytes _PUB_BYTES_INTERNAL $ \out -> @@ -249,6 +348,9 @@ xonly (Context tex) (Pub pub) = -- an x-only public key. -- -- The size of the input is not checked. +-- +-- >>> wcontext $ \tex -> parse_xonly tex bytestring +-- "<bitcoin-core/secp256k1 x-only public key>" parse_xonly :: Context -> BS.ByteString -> IO XOnlyPub parse_xonly (Context tex) bs = A.allocaBytes _PUB_BYTES_INTERNAL $ \out -> @@ -261,6 +363,8 @@ parse_xonly (Context tex) bs = -- | Serialize an x-only public key into a 32-byte bytestring -- representation. +-- +-- >>> wcontext $ \tex -> serialize_xonly tex xonly serialize_xonly :: Context -> XOnlyPub -> IO BS.ByteString serialize_xonly (Context tex) (XOnlyPub pux) = A.allocaBytes _PUB_BYTES_XONLY $ \out -> do @@ -273,6 +377,9 @@ serialize_xonly (Context tex) (XOnlyPub pux) = -- | Derive a keypair from the provided 32-byte secret key. -- -- The size of the input is not checked. +-- +-- >>> wrcontext entropy $ \tex -> create_keypair tex sec +-- "<bitcoin-core/secp256k1 keypair>" create_keypair :: Context -> BS.ByteString -> IO KeyPair create_keypair (Context tex) sec = A.allocaBytes _KEYPAIR_BYTES $ \out -> @@ -284,6 +391,9 @@ create_keypair (Context tex) sec = pure (KeyPair per) -- | Extract a public key from a keypair. +-- +-- >>> wrcontext entropy $ \tex -> keypair_pub tex keypair +-- "<bitcoin-core/secp256k1 public key>" keypair_pub :: Context -> KeyPair -> IO Pub keypair_pub (Context tex) (KeyPair per) = A.allocaBytes _PUB_BYTES_INTERNAL $ \out -> @@ -295,6 +405,8 @@ keypair_pub (Context tex) (KeyPair per) = pure (Pub pub) -- | Extract a secret key from a keypair. +-- +-- >>> wrcontext entropy $ \tex -> keypair_sec tex keypair keypair_sec :: Context -> KeyPair -> IO BS.ByteString keypair_sec (Context tex) (KeyPair per) = A.allocaBytes _SEC_BYTES $ \out -> @@ -309,6 +421,8 @@ keypair_sec (Context tex) (KeyPair per) = -- secret key. -- -- The size of the input is not checked. +-- +-- >>> wrcontext entropy $ \tex -> ecdh tex pub sec ecdh :: Context -> Pub -> BS.ByteString -> IO BS.ByteString ecdh (Context tex) (Pub pub) sec = A.allocaBytes _SEC_BYTES $ \out -> @@ -323,7 +437,17 @@ ecdh (Context tex) (Pub pub) sec = -- | Sign a 32-byte message hash with the provided secret key. -- +-- BIP340 recommends that 32 bytes of auxiliary entropy be added when +-- signing, and bitcoin-core/secp256k1 allows this, but the added +-- entropy is only supplemental to security, and is not required. We +-- omit the feature here, for API simplicity. +-- +-- The resulting 64-byte signature is safe to serialize, and so is not +-- wrapped in a newtype. +-- -- The sizes of the inputs are not checked. +-- +-- >>> wrcontext entropy $ \tex -> sign_schnorr tex msg sec sign_schnorr :: Context -> BS.ByteString -> BS.ByteString -> IO BS.ByteString sign_schnorr c@(Context tex) msg sec = A.allocaBytes _SIG_BYTES $ \out -> @@ -339,8 +463,10 @@ sign_schnorr c@(Context tex) msg sec = -- hash with the supplied public key. -- -- The sizes of the inputs are not checked. -verify_schnorr :: Context -> BS.ByteString -> BS.ByteString -> Pub -> IO Bool -verify_schnorr c@(Context tex) sig msg pub = +-- +-- >>> wrcontext entropy $ \tex -> verify_schnorr tex pub msg sig +verify_schnorr :: Context -> Pub -> BS.ByteString -> BS.ByteString -> IO Bool +verify_schnorr c@(Context tex) pub msg sig = BS.useAsCString sig $ \(F.castPtr -> sip) -> BS.useAsCStringLen msg $ \(F.castPtr -> has, fromIntegral -> len) -> do XOnlyPub pux <- xonly c pub