diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/UNLICENSE b/UNLICENSE
new file mode 100644
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/secp256k1.cabal b/secp256k1.cabal
new file mode 100644
--- /dev/null
+++ b/secp256k1.cabal
@@ -0,0 +1,51 @@
+name:                secp256k1
+version:             0.1.0
+synopsis:            secp256k1 bindings for Haskell
+description:         Please see README.md
+homepage:            http://github.com/haskoin/secp256k1#readme
+license:             PublicDomain
+license-file:        UNLICENSE
+author:              Jean-Pierre Rupp
+maintainer:          root@haskoin.com
+copyright:           2015 Jean-Pierre Rupp
+category:            Crypto
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Crypto.Secp256k1
+                     , Crypto.Secp256k1.Internal
+  build-depends:       base >= 4.8 && < 5
+                     , bytestring
+                     , mtl
+                     , entropy
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+  extra-libraries:     secp256k1
+
+test-suite secp256k1-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       Crypto.Secp256k1.Tests
+                     , Crypto.Secp256k1.Internal.Tests
+  build-depends:       base
+                     , secp256k1
+                     , HUnit
+                     , QuickCheck
+                     , test-framework
+                     , test-framework-hunit
+                     , test-framework-quickcheck2
+                     , base16-bytestring
+                     , cryptohash
+                     , bytestring
+                     , mtl
+                     , entropy
+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/xenog/secp256k1
diff --git a/src/Crypto/Secp256k1.hs b/src/Crypto/Secp256k1.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Secp256k1.hs
@@ -0,0 +1,272 @@
+{-|
+Module      : Crypto.Secp256k1
+Description : Public SECP256K1 cryptographic functions
+License     : PublicDomain
+Maintainer  : root@haskoin.com
+Stability   : experimental
+Portability : POSIX
+
+This module exposes crytpographic functions from Bitcoin’s secp256k1 library.
+Depends on <https://github.com/bitcoin/secp256k1 secp256k1>.
+-}
+module Crypto.Secp256k1
+( -- * Messages
+  Msg, msg, getMsg
+  -- * Secret Keys
+, SecKey, secKey, getSecKey, importSecKey, exportSecKey, pubKey
+  -- * Public Keys
+, PubKey, importPubKey, exportPubKey
+  -- * Signatures
+, Sig, importSig, exportSig
+, signMsg, verifySig
+  -- * Addition & Multiplication
+, Tweak, tweak, getTweak
+, tweakAddSecKey, tweakMulSecKey
+, tweakAddPubKey, tweakMulPubKey
+, combinePubKeys
+) where
+
+import           Control.Monad.Reader
+import           Crypto.Secp256k1.Internal
+import           Data.ByteString           (ByteString)
+import qualified Data.ByteString           as BS
+import           Foreign
+import           System.IO.Unsafe
+
+-- | Internal public key data type.
+newtype PubKey = PubKey (ForeignPtr PubKey64)
+
+-- | Internal message data type.
+newtype Msg = Msg (ForeignPtr Msg32)
+
+-- | Internal signature data type.
+newtype Sig = Sig (ForeignPtr Sig64)
+
+-- | Internal secret key data type.
+newtype SecKey = SecKey (ForeignPtr SecKey32)
+
+-- | Internal tweak data type for addition and multiplication.
+newtype Tweak = Tweak (ForeignPtr Tweak32)
+
+instance Show PubKey where
+    show = show . exportPubKey True
+
+instance Show Msg where
+    show = show . getMsg
+
+instance Show Sig where
+    show = show . exportSig
+
+instance Show SecKey where
+    show = show . getSecKey
+
+instance Show Tweak where
+    show = show . getTweak
+
+instance Eq PubKey where
+    fp1 == fp2 = getPubKey fp1 == getPubKey fp2
+
+instance Eq Msg where
+    fm1 == fm2 = getMsg fm1 == getMsg fm2
+
+instance Eq Sig where
+    fg1 == fg2 = getSig fg1 == getSig fg2
+
+instance Eq SecKey where
+    fk1 == fk2 = getSecKey fk1 == getSecKey fk2
+
+instance Eq Tweak where
+    ft1 == ft2 = getTweak ft1 == getTweak ft2
+
+-- | Create internal message data from 32-byte 'ByteString'.
+msg :: ByteString -> Maybe Msg
+msg bs
+    | BS.length bs == 32 = unsafePerformIO $ do
+        fp <- mallocForeignPtr
+        withForeignPtr fp $ flip poke (Msg32 bs)
+        return $ Just $ Msg fp
+    | otherwise = Nothing
+
+-- | Create internal secret key data from 32-byte 'ByteString'.
+secKey :: ByteString -> Maybe SecKey
+secKey bs
+    | BS.length bs == 32 = unsafePerformIO $ do
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \p -> do
+            poke p (SecKey32 bs)
+            ec_seckey_verify ctx p
+        if isSuccess ret
+            then return $ Just $ SecKey fp
+            else return $ Nothing
+    | otherwise = Nothing
+
+-- | Create internal tweak data from 32-byte 'ByteString'.
+tweak :: ByteString -> Maybe Tweak
+tweak bs
+    | BS.length bs == 32 = unsafePerformIO $ do
+        fp <- mallocForeignPtr
+        withForeignPtr fp $ flip poke (Tweak32 bs)
+        return $ Just $ Tweak fp
+    | otherwise = Nothing
+
+-- | Get 32-byte secret key.
+getSecKey :: SecKey -> ByteString
+getSecKey (SecKey fk) = getSecKey32 $ unsafePerformIO $ withForeignPtr fk peek
+
+getPubKey :: PubKey -> ByteString
+getPubKey (PubKey fp) = getPubKey64 $ unsafePerformIO $ withForeignPtr fp peek
+
+getSig :: Sig -> ByteString
+getSig (Sig fg) = getSig64 $ unsafePerformIO $ withForeignPtr fg peek
+
+-- | Get 32-byte message.
+getMsg :: Msg -> ByteString
+getMsg (Msg fm) = getMsg32 $ unsafePerformIO $ withForeignPtr fm $ peek
+
+-- | Get 32-byte tweak.
+getTweak :: Tweak -> ByteString
+getTweak (Tweak ft) = getTweak32 $ unsafePerformIO $ withForeignPtr ft $ peek
+
+-- | Read DER-encoded public key.
+importPubKey :: ByteString -> Maybe PubKey
+importPubKey bs = unsafePerformIO $ do
+    useByteString bs $ \(b, l) -> do
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \p -> ec_pubkey_parse ctx p b l
+        if isSuccess ret then return $ Just $ PubKey fp else return Nothing
+
+-- | Encode public key as DER.  First argument 'True' for compressed output.
+exportPubKey :: Bool -> PubKey -> ByteString
+exportPubKey compress (PubKey pub) = unsafePerformIO $
+    withForeignPtr pub $ \p -> alloca $ \l -> allocaBytes 65 $ \o -> do
+        poke l 65
+        ret <- ec_pubkey_serialize ctx o l p c
+        unless (isSuccess ret) $ error "could not serialize public key"
+        n <- peek l
+        packByteString (o, n)
+  where
+    c = if compress then compressed else uncompressed
+
+-- | Read DER-encoded signature.
+importSig :: ByteString -> Maybe Sig
+importSig bs = unsafePerformIO $
+    useByteString bs $ \(b, l) -> do
+        fg <- mallocForeignPtr
+        ret <- withForeignPtr fg $ \g -> ecdsa_signature_parse_der ctx g b l
+        if isSuccess ret then return $ Just $ Sig fg else return Nothing
+
+-- | Encode signature as DER.
+exportSig :: Sig -> ByteString
+exportSig (Sig fg) = unsafePerformIO $
+    withForeignPtr fg $ \g -> alloca $ \l -> allocaBytes 72 $ \o -> do
+        poke l 72
+        ret <- ecdsa_signature_serialize_der ctx o l g
+        unless (isSuccess ret) $ error "could not serialize signature"
+        n <- peek l
+        packByteString (o, n)
+
+-- | Verify message signature. 'True' means that the signature is correct.
+verifySig :: PubKey -> Sig -> Msg -> Bool
+verifySig (PubKey fp) (Sig fg) (Msg fm) = unsafePerformIO $
+    withForeignPtr fp $ \p -> withForeignPtr fg $ \g ->
+        withForeignPtr fm $ \m -> isSuccess <$> ecdsa_verify ctx g m p
+
+-- | Sign message using secret key.
+signMsg :: SecKey -> Msg -> Sig
+signMsg (SecKey fk) (Msg fm) = unsafePerformIO $
+    withForeignPtr fk $ \k -> withForeignPtr fm $ \m -> do
+        fg <- mallocForeignPtr
+        ret <- withForeignPtr fg $ \g -> ecdsa_sign ctx g m k nullFunPtr nullPtr
+        unless (isSuccess ret) $ error "could not sign message"
+        return $ Sig fg
+
+-- | Obtain public key from secret key.
+pubKey :: SecKey -> PubKey
+pubKey (SecKey fk) = unsafePerformIO $
+    withForeignPtr fk $ \k -> do
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \p -> ec_pubkey_create ctx p k
+        unless (isSuccess ret) $ error "could not compute public key"
+        return $ PubKey fp
+
+-- | Read BER-encoded secret key.
+importSecKey :: ByteString -> Maybe SecKey
+importSecKey bs = unsafePerformIO $
+    useByteString bs $ \(b, l) -> do
+        fk <- mallocForeignPtr
+        ret <- withForeignPtr fk $ \k -> ec_privkey_import ctx k b l
+        if isSuccess ret then return $ Just $ SecKey fk else return Nothing
+
+-- | Encode secret key as BER.  First argument 'True' for compressed output.
+exportSecKey :: Bool -> SecKey -> ByteString
+exportSecKey compress (SecKey fk) = unsafePerformIO $
+    withForeignPtr fk $ \k -> alloca $ \l -> allocaBytes 279 $ \o -> do
+        poke l 279
+        ret <- ec_privkey_export ctx o l k c
+        unless (isSuccess ret) $ error "could not export secret key"
+        n <- peek l
+        packByteString (o, n)
+  where
+    c = if compress then compressed else uncompressed
+
+-- | Add tweak to secret key using ECDSA addition.
+tweakAddSecKey :: SecKey -> Tweak -> Maybe SecKey
+tweakAddSecKey (SecKey fk) (Tweak ft) = unsafePerformIO $
+    withForeignPtr fk $ \k -> withForeignPtr ft $ \t -> do
+        fk' <- mallocForeignPtr
+        ret <- withForeignPtr fk' $ \k' ->  do
+            key <- peek k
+            poke k' key
+            ec_privkey_tweak_add ctx k' t
+        if isSuccess ret then return $ Just $ SecKey fk' else return Nothing
+
+-- | Multiply secret key by tweak using ECDSA multiplication.
+tweakMulSecKey :: SecKey -> Tweak -> Maybe SecKey
+tweakMulSecKey (SecKey fk) (Tweak ft) = unsafePerformIO $
+    withForeignPtr fk $ \k -> withForeignPtr ft $ \t -> do
+        fk' <- mallocForeignPtr
+        ret <- withForeignPtr fk' $ \k' ->  do
+            key <- peek k
+            poke k' key
+            ec_privkey_tweak_mul ctx k' t
+        if isSuccess ret then return $ Just $ SecKey fk' else return Nothing
+
+-- | Perform ECDSA addition between the public key point and the point obtained
+-- by multiplying the tweak scalar by the curve generator.
+tweakAddPubKey :: PubKey -> Tweak -> Maybe PubKey
+tweakAddPubKey (PubKey fp) (Tweak ft) = unsafePerformIO $
+    withForeignPtr fp $ \p -> withForeignPtr ft $ \t -> do
+        fp' <- mallocForeignPtr
+        ret <- withForeignPtr fp' $ \p' ->  do
+            pub <- peek p
+            poke p' pub
+            ec_pubkey_tweak_add ctx p' t
+        if isSuccess ret then return $ Just $ PubKey fp' else return Nothing
+
+-- | Perform ECDSA multiplication between the public key point and the point
+-- obtained by multiplying the tweak scalar by the curve generator.
+tweakMulPubKey :: PubKey -> Tweak -> Maybe PubKey
+tweakMulPubKey (PubKey fp) (Tweak ft) = unsafePerformIO $
+    withForeignPtr fp $ \p -> withForeignPtr ft $ \t -> do
+        fp' <- mallocForeignPtr
+        ret <- withForeignPtr fp' $ \p' ->  do
+            pub <- peek p
+            poke p' pub
+            ec_pubkey_tweak_mul ctx p' t
+        if isSuccess ret then return $ Just $ PubKey fp' else return Nothing
+
+-- | Add multiple public keys together using ECDSA addition.
+combinePubKeys :: [PubKey] -> Maybe PubKey
+combinePubKeys pubs = unsafePerformIO $ pointers [] pubs $ \ps ->
+    allocaArray (length ps) $ \a -> do
+        pokeArray a ps
+        fp <- mallocForeignPtr
+        ret <- withForeignPtr fp $ \p ->
+            ec_pubkey_combine ctx p a (fromIntegral $ length ps)
+        if isSuccess ret
+            then return $ Just $ PubKey fp
+            else return Nothing
+  where
+    pointers ps [] f = f ps
+    pointers ps (PubKey fp : pubs') f =
+        withForeignPtr fp $ \p -> pointers (p:ps) pubs' f
diff --git a/src/Crypto/Secp256k1/Internal.hs b/src/Crypto/Secp256k1/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Secp256k1/Internal.hs
@@ -0,0 +1,344 @@
+{-|
+Module      : Crypto.Secp256k1.Internal
+Description : Internal SECP256K1 cryptographic functions
+License     : PublicDomain
+Maintainer  : root@haskoin.com
+Stability   : experimental
+Portability : POSIX
+
+The API in this module may change at any time.  This is an internal module
+only exposed for hacking and experimentation.
+-}
+module Crypto.Secp256k1.Internal where
+
+import           Control.Monad
+
+import           Data.ByteString        (ByteString, packCStringLen)
+import           Data.ByteString.Unsafe (unsafeUseAsCStringLen)
+
+import           Foreign
+import           Foreign.C
+
+import           System.Entropy
+import           System.IO.Unsafe
+
+data Ctx = Ctx
+
+newtype PubKey64 = PubKey64 { getPubKey64 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Msg32 = Msg32 { getMsg32 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Sig64 = Sig64 { getSig64 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Seed32 = Seed32 { getSeed32 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype SecKey32 = SecKey32 { getSecKey32 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Tweak32 = Tweak32 { getTweak32 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Nonce32 = Nonce32 { getNonce32 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Algo16 = Algo16 { getAlgo16 :: ByteString }
+    deriving (Read, Show, Eq, Ord)
+
+newtype CtxFlags = CtxFlags { getCtxFlags :: CUInt }
+    deriving (Read, Show, Eq, Ord)
+
+newtype SerFlags = SerFlags { getSerFlags :: CUInt }
+    deriving (Read, Show, Eq, Ord)
+
+newtype Ret = Ret { getRet :: CInt }
+    deriving (Read, Show, Eq, Ord)
+
+-- | Nonce32-generating function
+type NonceFunction a
+    =  Ptr Nonce32
+    -> Ptr Msg32
+    -> Ptr SecKey32
+    -> Ptr Algo16
+    -> Ptr a       -- ^ extra data
+    -> CUInt       -- ^ attempt
+    -> Ret
+
+verify :: CtxFlags
+verify = CtxFlags 1
+
+sign :: CtxFlags
+sign = CtxFlags 2
+
+signVerify :: CtxFlags
+signVerify = CtxFlags 3
+
+compressed :: SerFlags
+compressed = SerFlags 1
+
+uncompressed :: SerFlags
+uncompressed = SerFlags 0
+
+useByteString :: ByteString -> ((Ptr CUChar, CSize) -> IO a) -> IO a
+useByteString bs f =
+    unsafeUseAsCStringLen bs $ \(b, l) -> f (castPtr b, fromIntegral l)
+
+packByteString :: (Ptr CUChar, CSize) -> IO ByteString
+packByteString (b, l) = packCStringLen (castPtr b, fromIntegral l)
+
+instance Storable PubKey64 where
+    sizeOf _ = 64
+    alignment _ = 1
+    peek p = PubKey64 <$> packByteString (castPtr p, 64)
+    poke p (PubKey64 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 64
+
+instance Storable Sig64 where
+    sizeOf _ = 64
+    alignment _ = 1
+    peek p = Sig64 <$> packByteString (castPtr p, 64)
+    poke p (Sig64 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 64
+
+instance Storable Msg32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Msg32 <$> packByteString (castPtr p, 32)
+    poke p (Msg32 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Seed32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Seed32 <$> packByteString (castPtr p, 32)
+    poke p (Seed32 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable SecKey32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = SecKey32 <$> packByteString (castPtr p, 32)
+    poke p (SecKey32 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Tweak32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Tweak32 <$> packByteString (castPtr p, 32)
+    poke p (Tweak32 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Nonce32 where
+    sizeOf _ = 32
+    alignment _ = 1
+    peek p = Nonce32 <$> packByteString (castPtr p, 32)
+    poke p (Nonce32 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 32
+
+instance Storable Algo16 where
+    sizeOf _ = 16
+    alignment _ = 1
+    peek p = Algo16 <$> packByteString (castPtr p, 16)
+    poke p (Algo16 k) = useByteString k $
+        \(b, _) -> copyArray (castPtr p) b 16
+
+isSuccess :: Ret -> Bool
+isSuccess (Ret 0) = False
+isSuccess (Ret 1) = True
+isSuccess _ = undefined
+
+{-# NOINLINE ctx #-}
+ctx :: Ptr Ctx
+ctx = unsafePerformIO $ do
+    x <- context_create signVerify
+    e <- getEntropy 32
+    ret <- alloca $ \s -> poke s (Seed32 e) >> context_randomize x s
+    unless (isSuccess ret) $ error "failed to randomize context"
+    return x
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_create"
+    context_create
+    :: CtxFlags
+    -> IO (Ptr Ctx)
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_clone"
+    context_clone
+    :: Ptr Ctx
+    -> IO (Ptr Ctx)
+
+foreign import ccall
+    "secp256k1.h &secp256k1_context_destroy"
+    context_destroy
+    :: FunPtr (Ptr Ctx -> IO ())
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_set_illegal_callback"
+    set_illegal_callback
+    :: Ptr Ctx
+    -> FunPtr (CString -> Ptr a -> IO ()) -- ^ message, data
+    -> Ptr a                              -- ^ data
+    -> IO ()
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_set_error_callback"
+    set_error_callback
+    :: Ptr Ctx
+    -> FunPtr (CString -> Ptr a -> IO ()) -- ^ message, data
+    -> Ptr a                              -- ^ data
+    -> IO ()
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_parse"
+    ec_pubkey_parse
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr CUChar -- ^ encoded public key array
+    -> CSize      -- ^ size of encoded public key array
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_serialize"
+    ec_pubkey_serialize
+    :: Ptr Ctx
+    -> Ptr CUChar -- ^ array for encoded public key, must be large enough
+    -> Ptr CSize  -- ^ size of encoded public key, will be updated
+    -> Ptr PubKey64
+    -> SerFlags
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_parse_der"
+    ecdsa_signature_parse_der
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr CUChar -- ^ encoded DER signature
+    -> CSize      -- ^ size of encoded signature
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_signature_serialize_der"
+    ecdsa_signature_serialize_der
+    :: Ptr Ctx
+    -> Ptr CUChar -- ^ array for encoded signature, must be large enough
+    -> Ptr CSize  -- ^ size of encoded signature, will be updated
+    -> Ptr Sig64
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_verify"
+    ecdsa_verify
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr Msg32
+    -> Ptr PubKey64
+    -> IO Ret
+
+-- TODO:
+-- foreign import ccall
+--     "secp256k1.h &secp256k1_nonce_function_rfc6979"
+--     nonce_function_rfc6979
+--     :: FunPtr (NonceFunction Seed32)
+--
+-- TODO:
+-- foreign import ccall
+--     "secp256k1.h &secp256k1_nonce_function_default"
+--     nonce_function_default
+--     :: FunPtr (NonceFunction Seed32)
+
+foreign import ccall
+    "secp256k1.h secp256k1_ecdsa_sign"
+    ecdsa_sign
+    :: Ptr Ctx
+    -> Ptr Sig64
+    -> Ptr Msg32
+    -> Ptr SecKey32
+    -> FunPtr (NonceFunction a)
+    -> Ptr a -- ^ nonce data
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_seckey_verify"
+    ec_seckey_verify
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_create"
+    ec_pubkey_create
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr SecKey32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_privkey_export"
+    ec_privkey_export
+    :: Ptr Ctx
+    -> Ptr CUChar -- ^ array to store BER-encoded key (allocate 279 bytes)
+    -> Ptr CSize -- ^ size of previous array, will be updated
+    -> Ptr SecKey32
+    -> SerFlags
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_privkey_import"
+    ec_privkey_import
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> Ptr CUChar -- ^ BER-encoded private key
+    -> CSize
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_privkey_tweak_add"
+    ec_privkey_tweak_add
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_tweak_add"
+    ec_pubkey_tweak_add
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_privkey_tweak_mul"
+    ec_privkey_tweak_mul
+    :: Ptr Ctx
+    -> Ptr SecKey32
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_tweak_mul"
+    ec_pubkey_tweak_mul
+    :: Ptr Ctx
+    -> Ptr PubKey64
+    -> Ptr Tweak32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_context_randomize"
+    context_randomize
+    :: Ptr Ctx
+    -> Ptr Seed32
+    -> IO Ret
+
+foreign import ccall
+    "secp256k1.h secp256k1_ec_pubkey_combine"
+    ec_pubkey_combine
+    :: Ptr Ctx
+    -> Ptr PubKey64 -- ^ pointer to public key storage
+    -> Ptr (Ptr PubKey64) -- ^ pointer to array of public keys
+    -> CInt -- ^ number of public keys
+    -> IO Ret
diff --git a/test/Crypto/Secp256k1/Internal/Tests.hs b/test/Crypto/Secp256k1/Internal/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Crypto/Secp256k1/Internal/Tests.hs
@@ -0,0 +1,474 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Crypto.Secp256k1.Internal.Tests (tests) where
+
+import           Control.Monad
+import           Control.Monad.Trans
+import           Crypto.Secp256k1.Internal
+import           Data.ByteString                (packCStringLen,
+                                                 useAsCStringLen)
+import qualified Data.ByteString.Base16         as B16
+import           Foreign
+import           System.Entropy
+import           Test.Framework                 (Test, testGroup)
+import           Test.Framework.Providers.HUnit (testCase)
+import           Test.HUnit                     (Assertion, assertBool,
+                                                 assertEqual)
+
+tests :: [Test]
+tests =
+    [ testGroup "Housekeeping"
+        [ testCase "Create context"           create_context_test
+        , testCase "Randomize context"        randomize_context_test
+        , testCase "Clone context"            clone_context_test
+        -- TODO:
+        -- , testCase "Set illegal callback" set_illegal_callback_test
+        -- TODO:
+        -- , testCase "Set error callback" set_error_callback_test
+        ]
+    , testGroup "Serialization"
+        [ testCase "Parse public key"         ec_pubkey_parse_test
+        , testCase "Serialize public key"     ec_pubkey_serialize_test
+        , testCase "Storable public key"      pubkey_storable_test
+        , testCase "Parse DER signature"      ecdsa_signature_parse_der_test
+        , testCase "Serialize DER signature"  ecdsa_signature_serialize_der_test
+        ]
+    , testGroup "Signatures"
+        [ testCase "ECDSA verify"             ecdsa_verify_test
+        -- TODO:
+        -- , testCase "RFC6979 nonce function"   nonce_function_rfc6979_test
+        , testCase "ECDSA sign"               ecdsa_sign_test
+        ]
+    , testGroup "Secret keys"
+        [ testCase "Verify secret key"        ec_seckey_verify_test
+        , testCase "Create public key"        ec_pubkey_create_test
+        , testCase "Serialize BER secret key" ec_privkey_export_test
+        , testCase "Import BER secret key"    ec_privkey_import_test
+        , testCase "Tweak add secret key"     ec_privkey_tweak_add_test
+        , testCase "Tweak mult. secret key"   ec_privkey_tweak_mul_test
+        ]
+    , testGroup "Public keys"
+        [ testCase "Tweak add public key"     ec_pubkey_tweak_add_test
+        , testCase "Tweak mult. public key"   ec_pubkey_tweak_mul_test
+        , testCase "Combine public keys"      ec_pubkey_combine_test
+        ]
+    ]
+
+withEntropy :: (Ptr Seed32 -> IO a) -> IO a
+withEntropy f = getEntropy 32 >>= \e -> alloca $ \s -> poke s (Seed32 e) >> f s
+
+create_context_test :: Assertion
+create_context_test = do
+    context_ptr <- liftIO $ context_create signVerify
+    assertBool "context not null" $ context_ptr /= nullPtr
+
+randomize_context_test :: Assertion
+randomize_context_test = do
+    ret <- liftIO $ context_create sign >>= \x ->
+        withEntropy (context_randomize x)
+    assertBool "context randomized" $ isSuccess ret
+
+clone_context_test :: Assertion
+clone_context_test = do
+    (x1, x2) <- liftIO $ do
+        x1 <- context_create signVerify
+        ret <- withEntropy $ context_randomize x1
+        unless (isSuccess ret) $ error "failed to randomize context"
+        x2 <- context_clone(x1)
+        return (x1, x2)
+    assertBool "original context not null" $ x1 /= nullPtr
+    assertBool "cloned context not null" $ x2 /= nullPtr
+    assertBool "context ptrs different" $ x1 /= x2
+
+ec_pubkey_parse_test :: Assertion
+ec_pubkey_parse_test = do
+    ret <- liftIO $ useAsCStringLen der $ \(i, il) -> do
+        x <- context_create verify
+        alloca $ \pubkey ->
+            ec_pubkey_parse x pubkey (castPtr i) (fromIntegral il)
+    assertBool "parsed public key" (isSuccess ret)
+  where
+    der = fst $ B16.decode
+        "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+ec_pubkey_serialize_test :: Assertion
+ec_pubkey_serialize_test = do
+    (ret, dec) <- liftIO $ useAsCStringLen der $ \(i, il) ->
+        alloca $ \k -> alloca $ \ol -> allocaBytes 72 $ \o -> do
+        poke ol 72
+        x <- context_create verify
+        ret1 <-
+            ec_pubkey_parse x k (castPtr i) (fromIntegral il)
+        unless (isSuccess ret1) $ error "failed to parse pubkey"
+        ret2 <- ec_pubkey_serialize
+            x o ol k compressed
+        len <- fromIntegral <$> peek ol
+        decoded <- packCStringLen (castPtr o, len)
+        return (ret2, decoded)
+    assertBool  "serialized public key successfully" $ isSuccess ret
+    assertEqual "public key matches" der dec
+  where
+    der = fst $ B16.decode
+        "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+pubkey_storable_test :: Assertion
+pubkey_storable_test = do
+    (pk1, pk2, dec) <- liftIO $ useAsCStringLen der $ \(i, il) -> do
+        x <- context_create verify
+        pk1 <- alloca $ \pk -> do
+            ret <-
+                ec_pubkey_parse x pk (castPtr i) (fromIntegral il)
+            unless (isSuccess ret) $ error "failed to parse pubkey"
+            peek pk
+        (pk2, dec) <- alloca $ \pk -> alloca $ \ol -> allocaBytes 72 $ \o -> do
+            poke ol 72
+            poke pk pk1
+            ret <-
+                ec_pubkey_serialize x o ol pk compressed
+            unless (isSuccess ret) $ error "failed to serialize pubkey"
+            len <- fromIntegral <$> peek ol
+            dec <- packCStringLen (castPtr o, len)
+            pk2 <- peek pk
+            return (pk2, dec)
+        return (pk1, pk2, dec)
+    assertEqual "poke/peek public key" pk1 pk2
+    assertEqual "public key matches" der dec
+  where
+    der = fst $ B16.decode
+        "03dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705"
+
+ecdsa_signature_parse_der_test :: Assertion
+ecdsa_signature_parse_der_test = do
+    ret <- liftIO $ useAsCStringLen der $ \(d, dl) -> alloca $ \s -> do
+        x <- context_create verify
+        ecdsa_signature_parse_der x s (castPtr d) (fromIntegral dl)
+    assertBool "parsed signature successfully" $ isSuccess ret
+  where
+    der = fst $ B16.decode $
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+
+ecdsa_signature_serialize_der_test :: Assertion
+ecdsa_signature_serialize_der_test = do
+    (ret, enc) <- liftIO $ do
+        x <- context_create verify
+        sig <- useAsCStringLen der $ \(d, dl) -> alloca $ \s -> do
+            ret <-
+                ecdsa_signature_parse_der x s (castPtr d) (fromIntegral dl)
+            unless (isSuccess ret) $ error "could not parse DER"
+            peek s
+        alloca $ \s -> alloca $ \ol -> allocaBytes 72 $ \o -> do
+            poke ol 72
+            poke s sig
+            ret <-
+                ecdsa_signature_serialize_der x o ol s
+            len <- fromIntegral <$> peek ol
+            enc <- packCStringLen (castPtr o, len)
+            return (ret, enc)
+    assertBool  "serialization successful" $ isSuccess ret
+    assertEqual "signatures match" der enc
+  where
+    der = fst $ B16.decode $
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+
+ecdsa_verify_test :: Assertion
+ecdsa_verify_test = do
+    ret <- liftIO $ do
+        x <- context_create verify
+        sig <- useAsCStringLen der $ \(d, dl) -> alloca $ \s -> do
+            ret <-
+                ecdsa_signature_parse_der x s (castPtr d) (fromIntegral dl)
+            unless (isSuccess ret) $ error "could not parse DER"
+            peek s
+        pk <- useAsCStringLen pub $ \(p, pl) -> alloca $ \k -> do
+            ret <-
+                ec_pubkey_parse x k (castPtr p) (fromIntegral pl)
+            unless (isSuccess ret) $ error "could not parse public key"
+            peek k
+        alloca $ \m -> alloca $ \k -> alloca $ \s -> do
+            poke m msg
+            poke k pk
+            poke s sig
+            ecdsa_verify x s m k
+    assertBool "signature valid" $ isSuccess ret
+  where
+    der = fst $ B16.decode $
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+    pub = fst $ B16.decode $
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    msg = Msg32 $ fst $ B16.decode $
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+
+-- TODO:
+-- nonce_function_rfc6979_test :: Assertion
+-- nonce_function_rfc6979_test = do
+--     (ret, nonce) <- alloca $ \n -> alloca $ \m -> alloca $ \k -> do
+--             poke m msg
+--             poke k key
+--             let ret = mkNonceFunction
+--                     nonce_function_rfc6979 n m k nullPtr nullPtr 0
+--             nonce <- peek n
+--             return (ret, nonce)
+--     assertBool "nonce calculated" $ isSuccess ret
+--     assertEqual "nonce correct" expected nonce
+--   where
+--     msg = Msg32 $ fst $ B16.decode $
+--         "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+--     key = SecKey32 $ fst $ B16.decode $
+--         "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+--     expected = Nonce $ fst $ B16.decode $
+--         "8f30c2bf5a3e1199b19a6703fa578376e5225d6ecfc7ecf817aa8b8b16203d64"
+
+ecdsa_sign_test :: Assertion
+ecdsa_sign_test = do
+    (ret, sig) <- liftIO $ do
+        x <- context_create sign
+        retr <- withEntropy $ context_randomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        alloca $ \s -> alloca $ \m -> alloca $ \k -> alloca $ \ol ->
+            allocaBytes 72 $ \o -> do
+                poke ol 72
+                poke m msg
+                poke k key
+                ret1 <-
+                    -- TODO:
+                    -- ecdsa_sign x s m k nonce_function_default nullPtr
+                    ecdsa_sign x s m k nullFunPtr nullPtr
+                ret2 <- ecdsa_signature_serialize_der x o ol s
+                unless (isSuccess ret2) $error "could not serialize signature"
+                len <- peek ol
+                sig <- packCStringLen (castPtr o, fromIntegral len)
+                return (ret1, sig)
+    assertBool "successful signing" $ isSuccess ret
+    assertEqual "signature matches" sig der
+  where
+    msg = Msg32 $ fst $ B16.decode $
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    der = fst $ B16.decode $
+        "3045022100f502bfa07af43e7ef265618b0d929a7619ee01d6150e37eb6eaaf2c8bd37\
+        \fb2202206f0415ab0e9a977afd78b2c26ef39b3952096d319fd4b101c768ad6c132e30\
+        \45"
+
+ec_seckey_verify_test :: Assertion
+ec_seckey_verify_test = do
+    ret <- liftIO $ alloca $ \k -> do
+        poke k key
+        x <- context_create sign
+        retr <- withEntropy $ context_randomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        ec_seckey_verify x k
+    assertBool "valid secret key" $ isSuccess ret
+  where
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+
+ec_pubkey_create_test :: Assertion
+ec_pubkey_create_test = do
+    (ret, pk) <- liftIO $ alloca $ \p -> alloca $ \k -> do
+        poke k key
+        x <- context_create sign
+        retr <- withEntropy $ context_randomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        ret <- ec_pubkey_create x p k
+        allocaBytes 65 $ \o -> alloca $ \ol -> do
+            poke ol 65
+            rets <- ec_pubkey_serialize
+                x o ol p uncompressed
+            unless (isSuccess rets) $ error "failed to serialive public key"
+            len <- fromIntegral <$> peek ol
+            pk <- packCStringLen (castPtr o, len)
+            return (ret, pk)
+    assertBool "successful pubkey creation" $ isSuccess ret
+    assertEqual "public key matches" pub pk
+  where
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    pub = fst $ B16.decode $
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+
+ec_privkey_export_test :: Assertion
+ec_privkey_export_test = do
+    ret <- liftIO $ alloca $ \k ->
+        allocaBytes 279 $ \o -> alloca $ \ol -> do
+            poke ol 279
+            poke k key
+            x <- context_create sign
+            retr <- withEntropy $ context_randomize x
+            unless (isSuccess retr) $ error "failed to randomize context"
+            ec_privkey_export x o ol k uncompressed
+    assertBool "successful secret key BER serialization" $ isSuccess ret
+  where
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+
+ec_privkey_import_test :: Assertion
+ec_privkey_import_test = do
+    (ret, dec) <- liftIO $ do
+        x <- context_create sign
+        retr <- withEntropy $ context_randomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        ber <- allocaBytes 279 $ \o -> alloca $ \ol -> alloca $ \k -> do
+            poke ol 279
+            poke k key
+            rets <- ec_privkey_export x o ol k uncompressed
+            unless (isSuccess rets) $ error "failed to serialize key"
+            len <- fromIntegral <$> peek ol
+            packCStringLen (castPtr o, len)
+        useAsCStringLen ber $ \(b, bl) -> alloca $ \k -> do
+            ret <- ec_privkey_import x k (castPtr b) (fromIntegral bl)
+            dec <- peek k
+            return (ret, dec)
+    assertBool "successful secret key BER deserialization" $ isSuccess ret
+    assertEqual "keys match" key dec
+  where
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+
+ec_privkey_tweak_add_test :: Assertion
+ec_privkey_tweak_add_test = do
+    (ret, tweaked) <- liftIO $ do
+        x <- context_create sign
+        retr <- withEntropy $ context_randomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        alloca $ \w -> alloca $ \k -> do
+            poke w tweak
+            poke k key
+            ret <- ec_privkey_tweak_add x k w
+            tweaked <- peek k
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    tweak = Tweak32 $ fst $ B16.decode $
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = SecKey32 $ fst $ B16.decode $
+        "ec1e3ce1cefa18a671d51125e2b249688d934b0e28f5d1665384d9b02f929059"
+
+ec_privkey_tweak_mul_test :: Assertion
+ec_privkey_tweak_mul_test = do
+    (ret, tweaked) <- liftIO $ do
+        x <- context_create sign
+        retr <- withEntropy $ context_randomize x
+        unless (isSuccess retr) $ error "failed to randomize context"
+        alloca $ \w -> alloca $ \k -> do
+            poke w tweak
+            poke k key
+            ret <- ec_privkey_tweak_mul x k w
+            tweaked <- peek k
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    key = SecKey32 $ fst $ B16.decode $
+        "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+    tweak = Tweak32 $ fst $ B16.decode $
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = SecKey32 $ fst $ B16.decode $
+        "a96f5962493acb179f60a86a9785fc7a30e0c39b64c09d24fe064d9aef15e4c0"
+
+ec_pubkey_tweak_add_test :: Assertion
+ec_pubkey_tweak_add_test = do
+    (ret, tweaked) <- liftIO $ do
+        x <- context_create verify
+        pk <- alloca $ \p -> useAsCStringLen pub $ \(d, dl) -> do
+            ret <- ec_pubkey_parse x p (castPtr d) (fromIntegral dl)
+            unless (isSuccess ret) $ error "could not parse public key"
+            peek p
+        alloca $ \w -> alloca $ \p -> allocaBytes 72 $ \d -> alloca $ \dl -> do
+            poke w tweak
+            poke p pk
+            poke dl 72
+            ret <- ec_pubkey_tweak_add x p w
+            rets <- ec_pubkey_serialize x d dl p uncompressed
+            unless (isSuccess rets) $ error "could not serialize public key"
+            len <- peek dl
+            tweaked <- packCStringLen (castPtr d, fromIntegral len)
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    pub = fst $ B16.decode $
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    tweak = Tweak32 $ fst $ B16.decode $
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = fst $ B16.decode $
+        "04441c3982b97576646e0df0c96736063df6b42f2ee566d13b9f6424302d1379e518fd\
+        \c87a14c5435bff7a5db4552042cb4120c6b86a4bbd3d0643f3c14ad01368"
+
+ec_pubkey_tweak_mul_test :: Assertion
+ec_pubkey_tweak_mul_test = do
+    (ret, tweaked) <- liftIO $ do
+        x <- context_create verify
+        pk <- alloca $ \p -> useAsCStringLen pub $ \(d, dl) -> do
+            ret <- ec_pubkey_parse x p (castPtr d) (fromIntegral dl)
+            unless (isSuccess ret) $ error "could not parse public key"
+            peek p
+        alloca $ \w -> alloca $ \p -> allocaBytes 72 $ \d -> alloca $ \dl -> do
+            poke w tweak
+            poke p pk
+            poke dl 72
+            ret <- ec_pubkey_tweak_mul x p w
+            rets <- ec_pubkey_serialize x d dl p uncompressed
+            unless (isSuccess rets) $ error "could not serialize public key"
+            len <- peek dl
+            tweaked <- packCStringLen (castPtr d, fromIntegral len)
+            return (ret, tweaked)
+    assertBool "successful secret key tweak" $ isSuccess ret
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    pub = fst $ B16.decode $
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    tweak = Tweak32 $ fst $ B16.decode $
+        "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+    expected = fst $ B16.decode $
+        "04f379dc99cdf5c83e433defa267fbb3377d61d6b779c06a0e4ce29ae3ff5353b12ae4\
+        \9c9d07e7368f2ba5a446c203255ce912322991a2d6a9d5d5761c61ed1845"
+
+ec_pubkey_combine_test :: Assertion
+ec_pubkey_combine_test = do
+    (ret, com) <- liftIO $ alloca $ \p1 -> alloca $ \p2 -> alloca $ \p3 ->
+        allocaBytes 72 $ \d -> alloca $ \dl -> allocaArray 3 $ \a ->
+            alloca $ \p -> do
+                x <- context_create verify
+                parse x pub1 p1
+                parse x pub2 p2
+                parse x pub3 p3
+                pokeArray a [p1, p2, p3]
+                poke dl 72
+                ret <- ec_pubkey_combine x p a 3
+                rets <- ec_pubkey_serialize
+                    x d dl p uncompressed
+                unless (isSuccess rets) $ error "could not serialize public key"
+                len <- peek dl
+                com <- packCStringLen (castPtr d, fromIntegral len)
+                return (ret, com)
+    assertBool "successful key combination" $ isSuccess ret
+    assertEqual "combined keys match" expected com
+  where
+    parse x pub p = useAsCStringLen pub $ \(d, dl) -> do
+        ret <- ec_pubkey_parse x p (castPtr d) (fromIntegral dl)
+        unless (isSuccess ret) $ error "could not parse public key"
+    pub1 = fst $ B16.decode $
+        "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd447051221\
+        \3d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+    pub2 = fst $ B16.decode $
+        "0487d82042d93447008dfe2af762068a1e53ff394a5bf8f68a045fa642b99ea5d153f5\
+        \77dd2dba6c7ae4cfd7b6622409d7edd2d76dd13a8092cd3af97b77bd2c77"
+    pub3 = fst $ B16.decode $
+        "049b101edcbe1ee37ff6b2318526a425b629e823d7d8d9154417880595a28000ee3feb\
+        \d908754b8ce4e491aa6fe488b41fb5d4bb3788e33c9ff95a7a9229166d59"
+    expected = fst $ B16.decode $
+        "043d9a7ec70011efc23c33a7e62d2ea73cca87797e3b659d93bea6aa871aebde56c3bc\
+        \6134ca82e324b0ab9c0e601a6d2933afe7fb5d9f3aae900f5c5dc6e362c8"
diff --git a/test/Crypto/Secp256k1/Tests.hs b/test/Crypto/Secp256k1/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Crypto/Secp256k1/Tests.hs
@@ -0,0 +1,161 @@
+module Crypto.Secp256k1.Tests (tests) where
+
+import           Crypto.Secp256k1
+import qualified Data.ByteString                      as BS
+import qualified Data.ByteString.Base16               as B16
+import qualified Data.ByteString.Char8                as B8
+import           Data.Maybe
+import           Test.Framework                       (Test, testGroup)
+import           Test.Framework.Providers.HUnit       (testCase)
+import           Test.Framework.Providers.QuickCheck2 (testProperty)
+import           Test.HUnit                           (Assertion, assertEqual)
+import           Test.QuickCheck
+
+tests :: [Test]
+tests =
+    [ testGroup "Signing"
+        [ testProperty "Signing messages" signMsgTest
+        , testProperty "Bad signatures" badSignatureTest
+        ]
+    , testGroup "Serialization"
+        [ testProperty "Serialize public key" serializePubKeyTest
+        , testProperty "Serialize signature" serializeSigTest
+        , testProperty "Serialize secret key" serializeSecKeyTest
+        ]
+    , testGroup "Tweaks"
+        [ testCase "Tweak add secret key" tweakAddSecKeyTest
+        , testCase "Tweak multiply secret key" tweakMulSecKeyTest
+        , testCase "Tweak add public key" tweakAddPubKeyTest
+        , testCase "Tweak multiply public key" tweakMulPubKeyTest
+        , testCase "Combine public keys" combinePubKeyTest
+        ]
+    ]
+
+newtype GenMsg = GenMsg Msg deriving (Show, Eq)
+newtype GenSecKey = GenSecKey SecKey deriving (Show, Eq)
+newtype GenPubKey = GenPubKey PubKey deriving (Show, Eq)
+
+instance Arbitrary GenMsg where
+    arbitrary = gen_msg
+      where
+        valid_bs = bs_gen `suchThat` isJust
+        bs_gen = (msg . BS.pack) <$> sequence (replicate 32 arbitrary)
+        gen_msg = (GenMsg . fromJust) <$> valid_bs
+
+instance Arbitrary GenSecKey where
+    arbitrary = gen_key
+      where
+        valid_bs = bs_gen `suchThat` isJust
+        bs_gen = (secKey . BS.pack) <$> sequence (replicate 32 arbitrary)
+        gen_key = (GenSecKey . fromJust) <$> valid_bs
+
+instance Arbitrary GenPubKey where
+    arbitrary = do
+        GenSecKey key <- arbitrary
+        return $ GenPubKey $ pubKey key
+
+signMsgTest :: (GenMsg, GenSecKey) -> Bool
+signMsgTest (GenMsg fm, GenSecKey fk) = verifySig fp fg fm where
+    fp = pubKey fk
+    fg = signMsg fk fm
+
+badSignatureTest :: (GenMsg, GenSecKey, GenPubKey) -> Bool
+badSignatureTest (GenMsg fm, GenSecKey fk, GenPubKey fp) =
+    not $ verifySig fp fg fm
+  where
+    fg = signMsg fk fm
+
+serializePubKeyTest :: (GenPubKey, Bool) -> Bool
+serializePubKeyTest (GenPubKey fp, b) =
+    case importPubKey $ exportPubKey b fp of
+        Just fp' -> fp == fp'
+        Nothing -> False
+
+serializeSigTest :: (GenMsg, GenSecKey) -> Bool
+serializeSigTest (GenMsg fm, GenSecKey fk) =
+    case importSig $ exportSig fg of
+        Just fg' -> fg == fg'
+        Nothing -> False
+  where
+    fg = signMsg fk fm
+
+serializeSecKeyTest :: (GenSecKey, Bool) -> Bool
+serializeSecKeyTest (GenSecKey fk, b) =
+    case importSecKey $ exportSecKey b fk of
+        Just fk' -> fk == fk'
+        Nothing -> False
+
+tweakAddSecKeyTest :: Assertion
+tweakAddSecKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        key <- secKey $ fst $ B16.decode $ B8.pack $
+            "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        twk <- tweak $ fst $ B16.decode $ B8.pack $
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakAddSecKey key twk
+    expected = secKey $ fst $ B16.decode $ B8.pack $
+        "ec1e3ce1cefa18a671d51125e2b249688d934b0e28f5d1665384d9b02f929059"
+
+tweakMulSecKeyTest :: Assertion
+tweakMulSecKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        key <- secKey $ fst $ B16.decode $ B8.pack $
+            "f65255094d7773ed8dd417badc9fc045c1f80fdc5b2d25172b031ce6933e039a"
+        twk <- tweak $ fst $ B16.decode $ B8.pack $
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakMulSecKey key twk
+    expected = secKey $ fst $ B16.decode $ B8.pack $
+        "a96f5962493acb179f60a86a9785fc7a30e0c39b64c09d24fe064d9aef15e4c0"
+
+tweakAddPubKeyTest :: Assertion
+tweakAddPubKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        pub <- importPubKey $ fst $ B16.decode $ B8.pack $
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705\
+            \12213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        twk <- tweak $ fst $ B16.decode $ B8.pack $
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakAddPubKey pub twk
+    expected = importPubKey $ fst $ B16.decode $ B8.pack $
+        "04441c3982b97576646e0df0c96736063df6b42f2ee566d13b9f6424302d1379e518fd\
+        \c87a14c5435bff7a5db4552042cb4120c6b86a4bbd3d0643f3c14ad01368"
+
+tweakMulPubKeyTest :: Assertion
+tweakMulPubKeyTest =
+    assertEqual "tweaked keys match" expected tweaked
+  where
+    tweaked = do
+        pub <- importPubKey $ fst $ B16.decode $ B8.pack $
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705\
+            \12213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        twk <- tweak $ fst $ B16.decode $ B8.pack $
+            "f5cbe7d88182a4b8e400f96b06128921864a18187d114c8ae8541b566c8ace00"
+        tweakMulPubKey pub twk
+    expected = importPubKey $ fst $ B16.decode $ B8.pack $
+        "04f379dc99cdf5c83e433defa267fbb3377d61d6b779c06a0e4ce29ae3ff5353b12ae4\
+        \9c9d07e7368f2ba5a446c203255ce912322991a2d6a9d5d5761c61ed1845"
+
+combinePubKeyTest :: Assertion
+combinePubKeyTest =
+    assertEqual "combined keys match" expected combined
+  where
+    combined = do
+        pub1 <- importPubKey $ fst $ B16.decode $ B8.pack
+            "04dded4203dac96a7e85f2c374a37ce3e9c9a155a72b64b4551b0bfe779dd44705\
+            \12213d5ed790522c042dee8e85c4c0ec5f96800b72bc5940c8bc1c5e11e4fcbf"
+        pub2 <- importPubKey $ fst $ B16.decode $ B8.pack
+            "0487d82042d93447008dfe2af762068a1e53ff394a5bf8f68a045fa642b99ea5d1\
+            \53f577dd2dba6c7ae4cfd7b6622409d7edd2d76dd13a8092cd3af97b77bd2c77"
+        pub3 <- importPubKey $ fst $ B16.decode $ B8.pack
+            "049b101edcbe1ee37ff6b2318526a425b629e823d7d8d9154417880595a28000ee\
+            \3febd908754b8ce4e491aa6fe488b41fb5d4bb3788e33c9ff95a7a9229166d59"
+        combinePubKeys [pub1, pub2, pub3]
+    expected = importPubKey $ fst $ B16.decode $ B8.pack
+        "043d9a7ec70011efc23c33a7e62d2ea73cca87797e3b659d93bea6aa871aebde56c3bc\
+        \6134ca82e324b0ab9c0e601a6d2933afe7fb5d9f3aae900f5c5dc6e362c8"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,8 @@
+import Test.Framework (defaultMain)
+
+-- Some simple signing tests
+import qualified Crypto.Secp256k1.Tests as T
+import qualified Crypto.Secp256k1.Internal.Tests as I
+
+main :: IO ()
+main = defaultMain $ T.tests ++ I.tests
