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