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