diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,3 +7,7 @@
 * `sodiumInit`
 * Random bytes generation: `Crypto.Random`
 * Key derivation: `Crypto.Key.derive` and `Crypto.Key.rederive`
+* MAC: `Crypto.Mac`, `Crypto.Mac.Lazy`
+* Random nonce generation: `Crypto.Nonce`
+* Public-key signatures: `Crypto.Sign`
+* Keypair generation from seed: `Crypto.Sign` and `Crypto.Encrypt.Public`
diff --git a/crypto-sodium.cabal b/crypto-sodium.cabal
--- a/crypto-sodium.cabal
+++ b/crypto-sodium.cabal
@@ -1,13 +1,13 @@
 cabal-version: 1.18
 
--- This file has been generated from package.yaml by hpack version 0.33.0.
+-- This file has been generated from package.yaml by hpack version 0.34.4.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 8f2d927fece7ed762df312f2f4393f5e471fe0068b66c7aa76c07a7ca34e220f
+-- hash: 25a103374b2b14f150bf4f1ccb243ed60f601e794e2345bee0adec07b08f3a47
 
 name:           crypto-sodium
-version:        0.0.3.1
+version:        0.0.4.0
 synopsis:       Easy-and-safe-to-use high-level cryptography based on Sodium
 description:    This is a collection of high-level cryptographic primitives based on
                 <https://libsodium.io/ Sodium>, spiced up with extra type-safety
@@ -41,11 +41,13 @@
                 .
                 == Secret-key cryptography
                 .
-                * Authenticated symmetric-key encryption: "Crypto.Encrypt.Secretbox"
+                * Authenticated symmetric-key encryption: "Crypto.Encrypt.Symmetric"
+                * Message authentication codes: "Crypto.Mac"
                 .
                 == Public-key cryptography
                 .
-                * Authenticated public-key encryption: "Crypto.Encrypt.Box"
+                * Authenticated public-key encryption: "Crypto.Encrypt.Public"
+                * Public-key signatures: "Crypto.Sign"
                 .
                 == Additional primitives
                 .
@@ -55,8 +57,8 @@
 homepage:       https://github.com/serokell/haskell-crypto#readme
 bug-reports:    https://github.com/serokell/haskell-crypto/issues
 author:         Kirill Elagin <kirelagin@serokell.io>
-maintainer:     Kirill Elagin <kirelagin@serokell.io>
-copyright:      2020 Serokell
+maintainer:     Serokell <libraries@serokell.io>
+copyright:      2021 Serokell
 license:        MPL-2.0
 license-file:   LICENSES/MPL-2.0.txt
 build-type:     Simple
@@ -70,55 +72,92 @@
 
 library
   exposed-modules:
-      Crypto.Encrypt.Box
-      Crypto.Encrypt.Secretbox
+      Crypto.Encrypt.Public
+      Crypto.Encrypt.Symmetric
+      Crypto.Encrypt.Symmetric.Stream
       Crypto.Init
+      Crypto.Internal.Random
+      Crypto.Internal.Verify
       Crypto.Key
       Crypto.Key.Internal
+      Crypto.Mac
+      Crypto.Nonce
       Crypto.Pwhash.Internal
       Crypto.Random
+      Crypto.Sign
   other-modules:
       Paths_crypto_sodium
   hs-source-dirs:
       lib
-  default-extensions: DataKinds FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores OverloadedStrings PolyKinds ScopedTypeVariables TypeApplications
+  default-extensions:
+      DataKinds
+      FlexibleContexts
+      FlexibleInstances
+      GeneralizedNewtypeDeriving
+      KindSignatures
+      LambdaCase
+      MultiParamTypeClasses
+      NamedFieldPuns
+      NumericUnderscores
+      OverloadedStrings
+      PolyKinds
+      ScopedTypeVariables
+      TypeApplications
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
   build-depends:
-      NaCl >=0.0.1.0 && <0.1
+      NaCl >=0.0.4.0 && <0.1
     , base >=4.10 && <4.15
     , bytestring >=0.9 && <0.11
     , cereal >=0.1 && <0.6
     , libsodium >=1.0.11 && <2
     , memory >=0.14.15 && <0.16
-    , safe-exceptions >=0.1 && <0.2
+    , random >=1.0 && <1.3
+    , safe-exceptions ==0.1.*
   default-language: Haskell2010
 
 test-suite test
   type: exitcode-stdio-1.0
   main-is: Test.hs
   other-modules:
+      Test.Crypto.Encrypt.Public
+      Test.Crypto.Encrypt.Symmetric
       Test.Crypto.Gen
       Test.Crypto.Key.Derivation
+      Test.Crypto.Nonce
       Test.Crypto.Pwhash
       Test.Crypto.Random
-      Test.Crypto.Secretbox
+      Test.Crypto.Sign
       Paths_crypto_sodium
   hs-source-dirs:
       test
-  default-extensions: DataKinds FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores OverloadedStrings PolyKinds ScopedTypeVariables TypeApplications
+  default-extensions:
+      DataKinds
+      FlexibleContexts
+      FlexibleInstances
+      GeneralizedNewtypeDeriving
+      KindSignatures
+      LambdaCase
+      MultiParamTypeClasses
+      NamedFieldPuns
+      NumericUnderscores
+      OverloadedStrings
+      PolyKinds
+      ScopedTypeVariables
+      TypeApplications
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
   build-tool-depends:
       tasty-discover:tasty-discover
   build-depends:
       HUnit
     , base >=4.10 && <4.15
-    , base16-bytestring
+    , base16 >=0.1.1 && <0.4
     , bytestring >=0.9 && <0.11
     , crypto-sodium
+    , deepseq
     , hedgehog
     , libsodium >=1.0.11 && <2
     , memory >=0.14.15 && <0.16
-    , safe-exceptions >=0.1 && <0.2
+    , safe-exceptions ==0.1.*
     , tasty
     , tasty-hedgehog
     , tasty-hunit
diff --git a/lib/Crypto/Encrypt/Box.hs b/lib/Crypto/Encrypt/Box.hs
deleted file mode 100644
--- a/lib/Crypto/Encrypt/Box.hs
+++ /dev/null
@@ -1,86 +0,0 @@
--- SPDX-FileCopyrightText: 2020 Serokell
---
--- SPDX-License-Identifier: MPL-2.0
-
-{-# OPTIONS_HADDOCK not-home #-}
-
--- ! This module merely re-exports definitions from the corresponding
--- ! module in NaCl and alters the Haddock to make it more specific
--- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
-
--- | Public-key authenticated encryption.
---
--- It is best to import this module qualified:
---
--- @
--- import qualified Crypto.Encrypt.Box as Box
---
--- encrypted = Box.'create' pk sk nonce message
--- decrypted = Box.'open' pk sk nonce encrypted
--- @
---
--- A box is an abstraction from NaCl. One way to think about it
--- is to imagine that you are putting data into a box protected by
--- the receiver’s public key and signed by your private key. The
--- receive will then be able to 'open' it using their private key
--- and your public key.
---
--- Note that this means that you need to exchange your public keys
--- in advance. It might seem strange at first that the receiver
--- needs to know your public key too, but this is actually very important
--- as otherwise the receiver would not be able to have any guarantees
--- regarding the source or the integrity of the data.
-module Crypto.Encrypt.Box
-  (
-  -- * Keys
-    PublicKey
-  , toPublicKey
-  , SecretKey
-  , toSecretKey
-  , keypair
-
-  -- * Nonce
-  , Nonce
-  , toNonce
-
-  -- * Encryption/decryption
-  , create
-  , open
-  ) where
-
-import Data.ByteArray (ByteArray, ByteArrayAccess)
-import Crypto.Box (Nonce, PublicKey, SecretKey, keypair, open, toNonce, toPublicKey, toSecretKey)
-
-import qualified Crypto.Box as NaCl.Box
-
-
--- | Encrypt a message.
---
--- @
--- encrypted = Box.create pk sk nonce message
--- @
---
--- *   @pk@ is the receiver’s public key, used for encryption.
---     @sk@ is the sender’s public key, used for authentication.
---
---     These are generated using 'keypair' and are supposed to be exchanged
---     in advance. Both parties need to know their own secret key and the other’s
---     public key.
---
--- *   @nonce@ is an extra noise that ensures that is required for security.
---     See "Crypto.Nonce" for how to work with it.
---
--- *   @message@ is the data you are encrypting.
---
--- This function adds authentication data, so if anyone modifies the cyphertext,
--- @open@ will refuse to decrypt it.
-create
-  ::  ( ByteArrayAccess nonceBytes
-      , ByteArrayAccess ptBytes, ByteArray ctBytes
-      )
-  => PublicKey  -- ^ Receiver’s public key
-  -> SecretKey  -- ^ Sender’s secret key
-  -> Nonce nonceBytes  -- ^ Nonce
-  -> ptBytes -- ^ Plaintext message
-  -> ctBytes
-create = NaCl.Box.create
diff --git a/lib/Crypto/Encrypt/Public.hs b/lib/Crypto/Encrypt/Public.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Encrypt/Public.hs
@@ -0,0 +1,154 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# OPTIONS_HADDOCK not-home #-}
+
+-- ! This module merely re-exports definitions from the corresponding
+-- ! module in NaCl and alters the Haddock to make it more specific
+-- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
+
+-- | Public-key authenticated encryption.
+--
+-- It is best to import this module qualified:
+--
+-- @
+-- import qualified Crypto.Encrypt.Public as Public
+--
+-- encrypted = Public.'encrypt' pk sk nonce message
+-- decrypted = Public.'decrypt' pk sk nonce encrypted
+-- @
+--
+-- A box is an abstraction from NaCl. One way to think about it
+-- is to imagine that you are putting data into a box protected by
+-- the receiver’s public key and signed by your private key. The
+-- receive will then be able to 'open' it using their private key
+-- and your public key.
+--
+-- Note that this means that you need to exchange your public keys
+-- in advance. It might seem strange at first that the receiver
+-- needs to know your public key too, but this is actually very important
+-- as otherwise the receiver would not be able to have any guarantees
+-- regarding the source or the integrity of the data.
+module Crypto.Encrypt.Public
+  (
+  -- * Keys
+    PublicKey
+  , toPublicKey
+  , SecretKey
+  , toSecretKey
+  , keypair
+  , keypairFromSeed
+  , unsafeKeypairFromSeed
+
+  -- * Nonce
+  , Nonce
+  , toNonce
+
+  -- * Encryption/decryption
+  , encrypt
+  , decrypt
+  ) where
+
+import Data.ByteArray (ByteArray, ByteArrayAccess, ScrubbedBytes, withByteArray)
+import Data.ByteArray.Sized as Sized (SizedByteArray, alloc, allocRet)
+import Data.ByteString (ByteString)
+import Data.Functor (void)
+import Data.Proxy (Proxy(..))
+import System.IO.Unsafe (unsafePerformIO)
+
+import qualified Libsodium as Na
+
+import NaCl.Box
+  (Nonce, PublicKey, SecretKey, keypair, toNonce, toPublicKey, toSecretKey)
+import qualified NaCl.Box as NaCl.Box
+
+
+-- | Encrypt a message.
+--
+-- @
+-- encrypted = Public.encrypt pk sk nonce message
+-- @
+--
+-- *   @pk@ is the receiver’s public key, used for encryption.
+--     @sk@ is the sender’s secret key, used for authentication.
+--
+--     These are generated using 'keypair' and are supposed to be exchanged
+--     in advance. Both parties need to know their own secret key and the other’s
+--     public key.
+--
+-- *   @nonce@ is an extra noise that ensures that is required for security.
+--     See "Crypto.Nonce" for how to work with it.
+--
+-- *   @message@ is the data you are encrypting.
+--
+-- This function adds authentication data, so if anyone modifies the cyphertext,
+-- 'decrypt' will refuse to decrypt it.
+encrypt
+  ::  ( ByteArrayAccess pkBytes, ByteArrayAccess skBytes
+      , ByteArrayAccess nonceBytes
+      , ByteArrayAccess ptBytes, ByteArray ctBytes
+      )
+  => PublicKey pkBytes  -- ^ Receiver’s public key
+  -> SecretKey skBytes  -- ^ Sender’s secret key
+  -> Nonce nonceBytes  -- ^ Nonce
+  -> ptBytes -- ^ Plaintext message
+  -> ctBytes
+encrypt = NaCl.Box.create
+
+
+-- | Decrypt a message.
+--
+-- @
+-- decrypted = Public.decrypt sk pk nonce encrypted
+-- @
+--
+-- * @sk@ is the receiver’s secret key, used for decription.
+-- * @pk@ is the sender’s public key, used for authentication.
+-- * @nonce@ is the same that was used for encryption.
+-- * @encrypted@ is the output of 'encrypt'.
+--
+-- This function will return @Nothing@ if the encrypted message was tampered
+-- with after it was encrypted.
+decrypt
+  ::  ( ByteArrayAccess skBytes, ByteArrayAccess pkBytes
+      , ByteArrayAccess nonceBytes
+      , ByteArray ptBytes, ByteArrayAccess ctBytes
+      )
+  => SecretKey skBytes  -- ^ Receiver’s secret key
+  -> PublicKey pkBytes  -- ^ Sender’s public key
+  -> Nonce nonceBytes  -- ^ Nonce
+  -> ctBytes -- ^ Encrypted message (cyphertext)
+  -> Maybe ptBytes
+decrypt = NaCl.Box.open
+
+
+-- | Seed for deterministically generating a keypair.
+--
+-- In accordance with Libsodium's documentation, the seed must be of size
+-- @Na.CRYPTO_BOX_SEEDBYTES@.
+--
+-- This type is parametrised by the actual data type that contains
+-- bytes. This can be, for example, a @ByteString@.
+type Seed a = SizedByteArray Na.CRYPTO_BOX_SEEDBYTES a
+
+
+-- | Generate a new 'SecretKey' together with its 'PublicKey' from a given seed.
+keypairFromSeed
+  :: ByteArrayAccess seed
+  => Seed seed
+  -> IO (PublicKey ByteString, SecretKey ScrubbedBytes)
+keypairFromSeed seed = do
+  allocRet Proxy $ \skPtr ->
+    alloc $ \pkPtr ->
+    withByteArray seed $ \sdPtr ->
+    -- always returns 0, so we don’t check it
+    void $ Na.crypto_box_seed_keypair pkPtr skPtr sdPtr
+
+-- | Generate a new 'SecretKey' together with its 'PublicKey' from a given seed,
+-- in a pure context.
+unsafeKeypairFromSeed
+  :: ByteArrayAccess seed
+  => Seed seed
+  -> (PublicKey ByteString, SecretKey ScrubbedBytes)
+unsafeKeypairFromSeed = unsafePerformIO . keypairFromSeed
diff --git a/lib/Crypto/Encrypt/Secretbox.hs b/lib/Crypto/Encrypt/Secretbox.hs
deleted file mode 100644
--- a/lib/Crypto/Encrypt/Secretbox.hs
+++ /dev/null
@@ -1,72 +0,0 @@
--- SPDX-FileCopyrightText: 2020 Serokell
---
--- SPDX-License-Identifier: MPL-2.0
-
-{-# OPTIONS_HADDOCK not-home #-}
-
--- ! This module merely re-exports definitions from the corresponding
--- ! module in NaCl and alters the Haddock to make it more specific
--- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
-
--- | Symmetric authenticated encryption.
---
--- It is best to import this module qualified:
---
--- @
--- import qualified Crypto.Encrypt.Secretbox as Secretbox
---
--- encrypted = Secretbox.'create' key nonce message
--- decrypted = Secretbox.'open' key nonce encrypted
--- @
---
--- A secretbox is an abstraction from NaCl. One way to think about it
--- is to imagine that you are putting data into a box protected by a
--- secret key. You 'create' such a box first, store it somewhere
--- (it is just a sequence of bytes), and when you need it in the
--- future, you 'open' it using the same secret key.
-module Crypto.Encrypt.Secretbox
-  (
-  -- * Keys
-    Key
-  , toKey
-
-  -- * Nonce
-  , Nonce
-  , toNonce
-
-  -- * Encryption/decryption
-  , create
-  , open
-  ) where
-
-import Crypto.Secretbox (Key, Nonce, open, toKey, toNonce)
-import Data.ByteArray (ByteArray, ByteArrayAccess)
-
-import qualified Crypto.Secretbox as NaCl.Secretbox
-
-
--- | Encrypt a message.
---
--- @
--- encrypted = Secretbox.create key nonce message
--- @
---
--- *   @key@ is the secret key used for encryption. See "Crypto.Key" for how
---     to get one.
---
--- *   @nonce@ is an extra noise that ensures that is required for security.
---     See "Crypto.Nonce" for how to work with it.
---
--- *   @message@ is the data you are encrypting.
---
--- This function adds authentication data, so if anyone modifies the cyphertext,
--- @open@ will refuse to decrypt it.
-create
-  ::  ( ByteArrayAccess keyBytes, ByteArrayAccess nonceBytes
-      , ByteArrayAccess ptBytes, ByteArray ctBytes
-      )
-  => Key keyBytes  -- ^ Secret key
-  -> Nonce nonceBytes  -- ^ Nonce
-  -> ptBytes -- ^ Plaintext message
-  -> ctBytes
-create = NaCl.Secretbox.create
diff --git a/lib/Crypto/Encrypt/Symmetric.hs b/lib/Crypto/Encrypt/Symmetric.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Encrypt/Symmetric.hs
@@ -0,0 +1,94 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# OPTIONS_HADDOCK not-home #-}
+
+-- ! This module merely re-exports definitions from the corresponding
+-- ! module in NaCl and alters the Haddock to make it more specific
+-- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
+
+-- | Symmetric authenticated encryption.
+--
+-- It is best to import this module qualified:
+--
+-- @
+-- import qualified Crypto.Encrypt.Symmetric as Symmetric
+--
+-- encrypted = Symmetric.'encrypt' key nonce message
+-- decrypted = Symmetric.'decrypt' key nonce encrypted
+-- @
+--
+-- In NaCl this is know as a “Secretbox”. One way to think about it
+-- is to imagine that you are putting data into a box protected by a
+-- secret key. You “create” such a box using 'encrypt', store it somewhere
+-- (it is just a sequence of bytes), and when you need it in the
+-- future, you “open” it with 'decrypt' using the same secret key.
+module Crypto.Encrypt.Symmetric
+  (
+  -- * Keys
+    Key
+  , toKey
+
+  -- * Nonce
+  , Nonce
+  , toNonce
+
+  -- * Encryption/decryption
+  , encrypt
+  , decrypt
+  ) where
+
+import NaCl.Secretbox (Key, Nonce, toKey, toNonce)
+import Data.ByteArray (ByteArray, ByteArrayAccess)
+
+import qualified NaCl.Secretbox as NaCl.Secretbox
+
+
+-- | Encrypt a message.
+--
+-- @
+-- encrypted = Symmetric.encrypt key nonce message
+-- @
+--
+-- *   @key@ is the secret key used for encryption. See "Crypto.Key" for how
+--     to get one.
+--
+-- *   @nonce@ is an extra noise that is required for security.
+--     See "Crypto.Nonce" for how to work with it.
+--
+-- *   @message@ is the data you are encrypting.
+--
+-- This function adds authentication data, so if anyone modifies the cyphertext,
+-- 'open' will refuse to decrypt it.
+encrypt
+  ::  ( ByteArrayAccess keyBytes, ByteArrayAccess nonceBytes
+      , ByteArrayAccess ptBytes, ByteArray ctBytes
+      )
+  => Key keyBytes  -- ^ Secret key
+  -> Nonce nonceBytes  -- ^ Nonce
+  -> ptBytes -- ^ Plaintext message
+  -> ctBytes
+encrypt = NaCl.Secretbox.create
+
+
+-- | Decrypt a message.
+--
+-- @
+-- decrypted = Symmetric.decrypt key nonce encrypted
+-- @
+--
+-- * @key@ and @nonce@ are the same that were used for encryption.
+-- * @encrypted@ is the output of 'create'.
+--
+-- This function will return @Nothing@ if the encrypted message was tampered
+-- with after it was encrypted.
+decrypt
+  ::  ( ByteArrayAccess keyBytes, ByteArrayAccess nonceBytes
+      , ByteArray ptBytes, ByteArrayAccess ctBytes
+      )
+  => Key keyBytes  -- ^ Secret key
+  -> Nonce nonceBytes  -- ^ Nonce
+  -> ctBytes -- ^ Encrypted message (cyphertext)
+  -> Maybe ptBytes
+decrypt = NaCl.Secretbox.open
diff --git a/lib/Crypto/Encrypt/Symmetric/Stream.hs b/lib/Crypto/Encrypt/Symmetric/Stream.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Encrypt/Symmetric/Stream.hs
@@ -0,0 +1,41 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# OPTIONS_HADDOCK not-home #-}
+
+-- | Symmetric authenticated encryption for streams.
+--
+-- This module provides generic types for Sodium-based streaming
+-- encryption. It does not provide any functionality itself.
+--
+-- There are separate packages that actually implement this functionality
+-- for specific streaming libraries:
+--
+-- * @crypto-sodium-streamly@ for @streamly@ streams.
+module Crypto.Encrypt.Symmetric.Stream
+  (
+  -- * Keys
+    Key
+  , toKey
+  ) where
+
+import Data.ByteArray (ByteArrayAccess)
+import Data.ByteArray.Sized (SizedByteArray, sizedByteArray)
+
+import qualified Libsodium as Na
+
+
+-- | Encryption key that can be used for streaming symmetric encryption.
+--
+-- This type is parametrised by the actual data type that contains
+-- bytes. This can be, for example, a @ByteString@, but, since this
+-- is a secret key, it is better to use @ScrubbedBytes@.
+type Key a = SizedByteArray Na.CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES a
+
+-- | Make a 'Key' from an arbitrary byte array.
+--
+-- This function returns @Just@ if and only if the byte array has
+-- the right length to be used as a key with a streaming symmetric encryption.
+toKey :: ByteArrayAccess ba => ba -> Maybe (Key ba)
+toKey = sizedByteArray
diff --git a/lib/Crypto/Internal/Random.hs b/lib/Crypto/Internal/Random.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Internal/Random.hs
@@ -0,0 +1,31 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Generate simple insecure random data.
+module Crypto.Internal.Random
+  ( generateInsecure
+  ) where
+
+import Data.ByteArray.Sized (SizedByteArray)
+import Data.ByteString (ByteString)
+import GHC.TypeLits (KnownNat)
+
+import Crypto.Random (generate)
+
+
+-- | Generate a sequence of random bytes.
+--
+-- The output of this function is NOT suitable for secret keys.
+generateInsecure
+  :: forall n. (KnownNat n)
+  => IO (SizedByteArray n ByteString)
+generateInsecure
+{-
+  = unsafeSizedByteArray . BS.pack . take len . randoms <$> newStdGen
+  where
+    len = fromIntegral $ natVal (Proxy :: Proxy n)
+-}
+  -- Haddock above is actually a lie. We use the same random generator
+  -- as for keys, because, after benchmarking, it happens to be faster :/.
+  = generate
diff --git a/lib/Crypto/Internal/Verify.hs b/lib/Crypto/Internal/Verify.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Internal/Verify.hs
@@ -0,0 +1,35 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | @crypto_verify_*@
+module Crypto.Internal.Verify
+  ( verifyBytes32
+  ) where
+
+import Data.ByteArray (ByteArrayAccess, withByteArray)
+import Data.ByteArray.Sized (SizedByteArray)
+
+import qualified Libsodium as Na
+
+
+-- | Compare two byte arrays of length 32.
+verifyBytes32
+  :: ( ByteArrayAccess ba1
+     , ByteArrayAccess ba2
+     )
+  => SizedByteArray 32 ba1  -- ^ First byte array
+  -> SizedByteArray 32 ba2  -- ^ Second byte array
+  -> IO Bool
+verifyBytes32 bytes1 bytes2 =
+  withByteArray bytes1 $ \ptr1 ->
+  withByteArray bytes2 $ \ptr2 -> do
+    -- TODO: I have no idea what I am doing
+    --
+    -- - Sodium also checks that pointers are different?
+    -- - This kind of double comparison was added in
+    --   c5a9d46386f917aa0ff1bfb711450f9af1d79a17
+    --   (why?)
+    res1 <- Na.crypto_verify_32 ptr1 ptr2
+    res2 <- Na.sodium_memcmp ptr2 ptr1 32
+    pure $ res1 == 0 && res2 == 0
diff --git a/lib/Crypto/Key.hs b/lib/Crypto/Key.hs
--- a/lib/Crypto/Key.hs
+++ b/lib/Crypto/Key.hs
@@ -94,7 +94,7 @@
 import Data.ByteArray.Sized (ByteArrayN, SizedByteArray)
 import Data.Kind (Constraint)
 import GHC.TypeLits (type (<=), KnownNat)
-import System.IO.Unsafe (unsafeDupablePerformIO)
+import System.IO.Unsafe (unsafePerformIO)
 
 import qualified Libsodium as Na
 
@@ -183,7 +183,7 @@
   -> passwd  -- ^ Password to rederive from.
   -> Maybe key
 rederive slip passwd =
-  unsafeDupablePerformIO $ I.rederive slip passwd
+  unsafePerformIO $ I.rederive slip passwd
   -- This IO is safe, because it is pure.
 
 
diff --git a/lib/Crypto/Key/Internal.hs b/lib/Crypto/Key/Internal.hs
--- a/lib/Crypto/Key/Internal.hs
+++ b/lib/Crypto/Key/Internal.hs
@@ -28,11 +28,11 @@
 
 import qualified Libsodium as Na
 
+import Crypto.Nonce (generate)
 import Crypto.Pwhash.Internal (Algorithm (Argon2id_1_3), Params (..), Salt, pwhash)
-import Crypto.Random (generate)
 
 
--- | Opaque bytes that contain the nonce and pwhash params.
+-- | Opaque bytes that contain the salt and pwhash params.
 type DerivationSlip = ByteString
 
 -- | Data contained in a derivation slip.
@@ -45,23 +45,23 @@
 -- used (for forward-compatibility).
 data DerivationSlipData = DerivationSlipData
   { params :: !Params
-  , nonce :: !(Salt ByteString)
+  , salt :: !(Salt ByteString)
   }
   deriving (Eq, Show)
 
 instance Serialize DerivationSlipData where
-  put (DerivationSlipData Params{opsLimit, memLimit} nonce) = do
+  put (DerivationSlipData Params{opsLimit, memLimit} salt) = do
     put (1 :: Word8)  -- algorithm marker for forward-compatibility
     put opsLimit >> put memLimit
-    put (unSizedByteArray nonce)
+    put (unSizedByteArray salt)
   get = do
     tag <- get @Word8
     when (tag /= 1) $ fail "Wrong algorithm parameters encoding tag"
     params <- Params <$> get <*> get
-    mnonce <- sizedByteArray <$> get @ByteString
-    case mnonce of
-      Nothing -> fail "Unexpected nonce size"
-      Just nonce -> pure $ DerivationSlipData params nonce
+    msalt <- sizedByteArray <$> get @ByteString
+    case msalt of
+      Nothing -> fail "Unexpected salt size"
+      Just salt -> pure $ DerivationSlipData params salt
 
 
 -- | Encode derivation slip data into bytes.
@@ -85,9 +85,9 @@
   -> passwd
   -> IO (Maybe (key, DerivationSlip))
 derive params passwd = do
-  nonce <- generate
-  mkey <- pwhash Argon2id_1_3 params passwd nonce
-  let slip = DerivationSlipData params nonce
+  salt <- generate
+  mkey <- pwhash Argon2id_1_3 params passwd salt
+  let slip = DerivationSlipData params salt
   pure $ fmap (, derivationSlipEncode slip) mkey
 
 -- | Derive the same key form the same password again.
@@ -102,5 +102,5 @@
 rederive slip passwd =
   case derivationSlipDecode slip of
     Nothing -> pure Nothing
-    Just (DerivationSlipData{params, nonce}) ->
-      pwhash Argon2id_1_3 params passwd nonce
+    Just (DerivationSlipData{params, salt}) ->
+      pwhash Argon2id_1_3 params passwd salt
diff --git a/lib/Crypto/Mac.hs b/lib/Crypto/Mac.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Mac.hs
@@ -0,0 +1,69 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# OPTIONS_HADDOCK not-home #-}
+
+-- ! This module merely re-exports definitions from the corresponding
+-- ! module in NaCl and alters the Haddock to make it more specific
+-- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
+
+-- | Message authentication codes.
+--
+-- It is best to import this module qualified:
+--
+-- @
+-- import qualified Crypto.Mac as Mac
+--
+-- authenticator = Mac.'create' key message
+-- if Mac.'verify' key message authenticator
+-- then {- Ok! -}
+-- else {- Fail! -}
+-- @
+--
+-- A message authenticator is like a signature, except that the key is
+-- secret. It can be used when it is not necessary to encrypt the data,
+-- but its integrity needs to be guaranteed.
+module Crypto.Mac
+  (
+  -- * Keys
+    Key
+  , toKey
+
+  -- * Authenticator tags
+  , Authenticator
+  , toAuthenticator
+
+  -- * Authentication
+  , create
+  , verify
+  ) where
+
+import NaCl.Auth (Authenticator, Key, toAuthenticator, toKey, verify)
+import Data.ByteArray (ByteArray, ByteArrayAccess)
+
+import qualified NaCl.Auth as NaCl.Auth
+
+
+-- | Create an authenticator for a message.
+--
+-- @
+-- authenticator = Mac.create key message
+-- @
+--
+-- *   @key@ is the secret key used for authentication. See "Crypto.Key" for how
+--     to get one.
+--
+-- *   @message@ is the data you are authenticating.
+--
+-- This function produces authentication data, so if anyone modifies the message,
+-- @verify@ will return @False@.
+create
+  ::  ( ByteArray authBytes
+      , ByteArrayAccess keyBytes
+      , ByteArrayAccess msg
+      )
+  => Key keyBytes  -- ^ Secret key.
+  -> msg  -- ^ Message to authenticate.
+  -> Authenticator authBytes
+create = NaCl.Auth.create
diff --git a/lib/Crypto/Nonce.hs b/lib/Crypto/Nonce.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Nonce.hs
@@ -0,0 +1,46 @@
+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | This module gives different ways of obtaining nonces.
+--
+-- A “nonce” is additional input provided to an encryption algorithm.
+-- The most important rule is that you cannot use the same nonce to encrypt
+-- more than one message. What will happen if you reuse a nonce depends
+-- on the details of the cryptographic algorithm, but, in general, expect
+-- the cryptography to fail completely in this case.
+--
+-- = Random nonce generation
+--
+-- The easiest way to guarantee that nonces do not repeat is to use a
+-- random nonce every time. If the nonce is large enough, then the
+-- probably of a random nonce repeating will be negligible.
+-- All cryptographic functions provided by this library can be
+-- safely used with random nonces.
+--
+-- The 'generate' function is great at generating new random nonces.
+-- The nonces are produced using the standard @random@ package, so
+-- the generation is very fast.
+module Crypto.Nonce
+  (
+  -- * Random nonce generation
+    generate
+  ) where
+
+import Data.ByteArray.Sized (SizedByteArray)
+import Data.ByteString (ByteString)
+import GHC.TypeLits (KnownNat)
+
+import Crypto.Internal.Random (generateInsecure)
+
+
+-- | Generate a new random nonce.
+--
+-- Note: random data generated by the functions in this module is only suitable
+-- to be used as a nonce, but never as a secret key.
+generate :: KnownNat n => IO (SizedByteArray n ByteString)
+generate = generateInsecure
diff --git a/lib/Crypto/Random.hs b/lib/Crypto/Random.hs
--- a/lib/Crypto/Random.hs
+++ b/lib/Crypto/Random.hs
@@ -15,7 +15,7 @@
 import qualified Libsodium as Na
 
 
--- | Generate a sequence of cryptographically-secure renadom bytes.
+-- | Generate a sequence of cryptographically-secure random bytes.
 --
 -- The output of this function is suitable to generate secret keys.
 --
diff --git a/lib/Crypto/Sign.hs b/lib/Crypto/Sign.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Sign.hs
@@ -0,0 +1,87 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# OPTIONS_HADDOCK not-home #-}
+
+-- ! This module merely re-exports definitions from the corresponding
+-- ! module in NaCl and alters the Haddock to make it more specific
+-- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
+
+-- | Public-key signatures.
+--
+-- It is best to import this module qualified:
+--
+-- @
+-- import qualified Crypto.Sign as Sign
+--
+-- signed = Sign.'create' sk message
+-- verified = Sign.'open' pk signed
+-- @
+--
+-- Functions in this modules work with /combined/ signatures.
+-- This means that when you sign a message, it will be copied as is
+-- and then a signature will be prepended. So you should treat the
+-- resulting value as a transparent (because it is not encrypted)
+-- package with a signature attached on top.
+--
+-- Instead of accessing the message directly, you should use
+-- 'open', which will verify the signature and return a copy of the
+-- original message only if the signature was valid.
+module Crypto.Sign
+  (
+  -- * Keys
+    PublicKey
+  , toPublicKey
+  , SecretKey
+  , toSecretKey
+  , keypair
+  , keypairFromSeed
+  , unsafeKeypairFromSeed
+
+  -- * Signing/verifying
+  , create
+  , open
+  ) where
+
+import Data.ByteArray (ByteArrayAccess, ScrubbedBytes, withByteArray)
+import Data.ByteString (ByteString)
+import Data.ByteArray.Sized (SizedByteArray, alloc, allocRet)
+import Data.Functor (void)
+import Data.Proxy (Proxy(..))
+import System.IO.Unsafe (unsafePerformIO)
+
+import qualified Libsodium as Na
+
+import NaCl.Sign
+  (PublicKey, SecretKey, create, keypair, open, toPublicKey, toSecretKey)
+
+-- | Seed for deterministically generating a keypair.
+--
+-- In accordance with Libsodium's documentation, the seed must be of size
+-- @Na.CRYPTO_SIGN_SEEDBYTES@.
+--
+-- This type is parametrised by the actual data type that contains
+-- bytes. This can be, for example, a @ByteString@.
+type Seed a = SizedByteArray Na.CRYPTO_SIGN_SEEDBYTES a
+
+
+-- | Generate a new 'SecretKey' together with its 'PublicKey' from a given seed.
+keypairFromSeed
+  :: ByteArrayAccess seed
+  => Seed seed
+  -> IO (PublicKey ByteString, SecretKey ScrubbedBytes)
+keypairFromSeed seed = do
+  allocRet Proxy $ \skPtr ->
+    alloc $ \pkPtr ->
+    withByteArray seed $ \sdPtr ->
+    -- always returns 0, so we don’t check it
+    void $ Na.crypto_sign_seed_keypair pkPtr skPtr sdPtr
+
+-- | Generate a new 'SecretKey' together with its 'PublicKey' from a given seed,
+-- in a pure context.
+unsafeKeypairFromSeed
+  :: ByteArrayAccess seed
+  => Seed seed
+  -> (PublicKey ByteString, SecretKey ScrubbedBytes)
+unsafeKeypairFromSeed = unsafePerformIO . keypairFromSeed
diff --git a/test/Test/Crypto/Encrypt/Public.hs b/test/Test/Crypto/Encrypt/Public.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Encrypt/Public.hs
@@ -0,0 +1,43 @@
+-- SPDX-FileCopyrightText: 2021 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | “Integration” tests: using Public with our helpers.
+module Test.Crypto.Encrypt.Public where
+
+import Hedgehog (Property, evalMaybe, forAll, property, tripping)
+import Hedgehog.Internal.Property (forAllT)
+
+import Control.Monad.IO.Class (liftIO)
+import Data.ByteArray.Sized (sizedByteArray)
+import Data.ByteString (ByteString)
+
+import qualified Libsodium as Na
+
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+
+import qualified Crypto.Encrypt.Public as Public
+
+
+nonceSize :: R.Range Int
+nonceSize = R.singleton $ fromIntegral Na.crypto_box_noncebytes
+
+seedSize :: R.Range Int
+seedSize = R.singleton $ fromIntegral Na.crypto_box_seedbytes
+
+
+hprop_encode_decode_seed :: Property
+hprop_encode_decode_seed = property $ do
+    seed1 <- evalMaybe . sizedByteArray =<< forAll (G.bytes seedSize)
+    seed2 <- evalMaybe . sizedByteArray =<< forAll (G.bytes seedSize)
+    (pkS, skS) <- forAllT . liftIO $ Public.keypairFromSeed seed1
+    (pkR, skR) <- forAllT . liftIO $ Public.keypairFromSeed seed2
+    nonceBytes <- forAll $ G.bytes nonceSize
+    let Just nonce = Public.toNonce nonceBytes
+    msg <- forAll $ G.bytes (R.linear 0 1_000)
+    tripping msg (encodeBs pkR skS nonce) (decodeBs skR pkS nonce)
+  where
+    -- We need to specify the type of the cyphertext as it is polymorphic
+    encodeBs pkR skS nonce msg = Public.encrypt pkR skS nonce msg :: ByteString
+    decodeBs skR pkS nonce ct = Public.decrypt skR pkS nonce ct :: Maybe ByteString
diff --git a/test/Test/Crypto/Encrypt/Symmetric.hs b/test/Test/Crypto/Encrypt/Symmetric.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Encrypt/Symmetric.hs
@@ -0,0 +1,32 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | “Integration” tests: using Symmetric with our helpers.
+module Test.Crypto.Encrypt.Symmetric where
+
+import Hedgehog (Property, forAll, property, tripping)
+import Hedgehog.Internal.Property (forAllT)
+
+import Control.Monad.IO.Class (liftIO)
+import Data.ByteString (ByteString)
+
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+
+import qualified Crypto.Key as Key (generate)
+import qualified Crypto.Random (generate)
+
+import qualified Crypto.Encrypt.Symmetric as Symmetric
+
+
+hprop_encode_decode :: Property
+hprop_encode_decode = property $ do
+    key <- forAllT $ liftIO Key.generate
+    nonce <- forAllT $ liftIO $ Crypto.Random.generate @ByteString
+    msg <- forAll $ G.bytes (R.linear 0 1_000)
+    tripping msg (encodeBs key nonce) (decodeBs key nonce)
+  where
+    -- We need to specify the type of the cyphertext as it is polymorphic
+    encodeBs key nonce msg = Symmetric.encrypt key nonce msg :: ByteString
+    decodeBs key nonce ct = Symmetric.decrypt key nonce ct :: Maybe ByteString
diff --git a/test/Test/Crypto/Nonce.hs b/test/Test/Crypto/Nonce.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Nonce.hs
@@ -0,0 +1,69 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+module Test.Crypto.Nonce where
+
+import Test.HUnit ((@?), (@?=), Assertion)
+
+import Control.DeepSeq (deepseq)
+import Data.ByteArray.Sized (unSizedByteArray)
+import Data.ByteString (ByteString)
+import Data.Ratio ((%))
+import System.CPUTime (getCPUTime)
+
+import qualified Data.ByteString as BS
+import qualified Libsodium as Na
+
+import Crypto.Nonce (generate)
+
+import qualified Crypto.Encrypt.Symmetric as Symmetric
+import qualified Crypto.Nonce as Nonce (generate)
+import qualified Crypto.Pwhash.Internal as Pwhash
+import qualified Crypto.Random as Random (generate)
+
+
+-- Well, this is kinda stupid, because we merely generate one random sequence,
+-- but this is just to check that the lengths are correctly propagated
+-- through types. So, good enough.
+
+unit_generate_Symmetric_nonce :: Assertion
+unit_generate_Symmetric_nonce = do
+  nonce <- generate :: IO (Symmetric.Nonce ByteString)
+  let bs = unSizedByteArray nonce
+  BS.length bs @?= fromIntegral Na.crypto_secretbox_noncebytes
+
+unit_generate_Pwhash_salt :: Assertion
+unit_generate_Pwhash_salt = do
+  nonce <- generate :: IO (Pwhash.Salt ByteString)
+  let bs = unSizedByteArray nonce
+  BS.length bs @?= fromIntegral Na.crypto_pwhash_saltbytes
+
+
+
+-- Benchmark to make sure this all makes sense and insecure nonse generation
+-- is actually faster than cryptographically-secure generation.
+unit_bench_against_crypto :: Assertion
+unit_bench_against_crypto = do
+    tNonce <- measure $ (unSizedByteArray <$> Nonce.generate @64)
+    tCrypto <- measure $ (unSizedByteArray <$> Random.generate @ByteString @64)
+    let ratio = fromRational (tNonce % tCrypto) :: Double
+
+    -- XXX: The benchmark is disabled, because we don’t yet have an
+    -- implementation that would actually be faster :/.
+    --        vvvvvvvv
+    ratio < 1 || True @? "Crypto gen is " <> show ratio <> "x faster"
+    --        ^^^^^^^^
+  where
+    measure act = do
+        t1 <- getCPUTime
+        go 1000
+        t2 <- getCPUTime
+        pure $ t2 - t1
+      where
+        go :: Int -> IO ()
+        go n
+          | n <= 0 = pure ()
+          | otherwise = do
+              res <- act
+              res `deepseq` go (n - 1)
diff --git a/test/Test/Crypto/Pwhash.hs b/test/Test/Crypto/Pwhash.hs
--- a/test/Test/Crypto/Pwhash.hs
+++ b/test/Test/Crypto/Pwhash.hs
@@ -13,10 +13,11 @@
 
 import Data.ByteArray.Sized (sizedByteArray)
 import Data.ByteString (ByteString)
+import Data.ByteString.Base16 (decodeBase16)
+import Data.Either (fromRight)
 import GHC.TypeLits (type (<=), KnownNat)
 
 import qualified Data.ByteString as BS
-import qualified Data.ByteString.Base16 as B16
 import qualified Libsodium as Na
 
 import Crypto.Pwhash.Internal (Algorithm (..), Params (Params), pwhash)
@@ -34,9 +35,9 @@
   -> Params  -- ^ Hashing params.
   -> Assertion
 pwhash_test_vector hash passwd salt alg params = do
-  let (hash', "") = B16.decode hash
-  let (passwd', "") = B16.decode passwd
-  let (salt', "") = B16.decode salt
+  let hash' = fromRight (error "impossible") . decodeBase16 $ hash
+  let passwd' = fromRight (error "impossible") . decodeBase16 $ passwd
+  let salt' = fromRight (error "impossible") . decodeBase16 $ salt
   let Just salt'N = sizedByteArray (BS.take 16 salt')  -- Note:
     -- for some reason, the test vectors in the file are 32 bytes long,
     -- while the pwhash function needs a 16-byte salt :/
diff --git a/test/Test/Crypto/Random.hs b/test/Test/Crypto/Random.hs
--- a/test/Test/Crypto/Random.hs
+++ b/test/Test/Crypto/Random.hs
@@ -14,7 +14,7 @@
 
 import Crypto.Random (generate)
 
-import qualified Crypto.Encrypt.Secretbox as Secretbox
+import qualified Crypto.Encrypt.Symmetric as Symmetric
 
 
 -- Well, this is kinda stupid, because we merely generate one random sequence,
@@ -23,14 +23,14 @@
 --
 -- Also it is not thread-safe, since we don’t call @sodiumInit@...
 
-unit_generate_Secretbox_key :: Assertion
-unit_generate_Secretbox_key = do
-  key <- generate :: IO (Secretbox.Key ByteString)
+unit_generate_Symmetric_key :: Assertion
+unit_generate_Symmetric_key = do
+  key <- generate :: IO (Symmetric.Key ByteString)
   let bs = unSizedByteArray key
   BS.length bs @?= fromIntegral Na.crypto_secretbox_keybytes
 
-unit_generate_Secretbox_nonce :: Assertion
-unit_generate_Secretbox_nonce = do
-  nonce <- generate :: IO (Secretbox.Nonce ByteString)
+unit_generate_Symmetric_nonce :: Assertion
+unit_generate_Symmetric_nonce = do
+  nonce <- generate :: IO (Symmetric.Nonce ByteString)
   let bs = unSizedByteArray nonce
   BS.length bs @?= fromIntegral Na.crypto_secretbox_noncebytes
diff --git a/test/Test/Crypto/Secretbox.hs b/test/Test/Crypto/Secretbox.hs
deleted file mode 100644
--- a/test/Test/Crypto/Secretbox.hs
+++ /dev/null
@@ -1,32 +0,0 @@
--- SPDX-FileCopyrightText: 2020 Serokell
---
--- SPDX-License-Identifier: MPL-2.0
-
--- | “Integration” tests: using Secretbox with our helpers.
-module Test.Crypto.Secretbox where
-
-import Hedgehog (Property, forAll, property, tripping)
-import Hedgehog.Internal.Property (forAllT)
-
-import Control.Monad.IO.Class (liftIO)
-import Data.ByteString (ByteString)
-
-import qualified Hedgehog.Gen as G
-import qualified Hedgehog.Range as R
-
-import qualified Crypto.Key as Key (generate)
-import qualified Crypto.Random (generate)
-
-import qualified Crypto.Encrypt.Secretbox as Secretbox
-
-
-hprop_encode_decode :: Property
-hprop_encode_decode = property $ do
-    key <- forAllT $ liftIO Key.generate
-    nonce <- forAllT $ liftIO $ Crypto.Random.generate @ByteString
-    msg <- forAll $ G.bytes (R.linear 0 1_000)
-    tripping msg (encodeBs key nonce) (decodeBs key nonce)
-  where
-    -- We need to specify the type of the cyphertext as it is polymorphic
-    encodeBs key nonce msg = Secretbox.create key nonce msg :: ByteString
-    decodeBs key nonce ct = Secretbox.open key nonce ct :: Maybe ByteString
diff --git a/test/Test/Crypto/Sign.hs b/test/Test/Crypto/Sign.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Sign.hs
@@ -0,0 +1,36 @@
+-- SPDX-FileCopyrightText: 2021 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | “Integration” tests: using Sign with our helpers.
+module Test.Crypto.Sign where
+
+import Hedgehog (Property, evalMaybe, forAll, property, tripping)
+import Hedgehog.Internal.Property (forAllT)
+
+import Control.Monad.IO.Class (liftIO)
+import Data.ByteArray.Sized (sizedByteArray)
+import Data.ByteString (ByteString)
+
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+
+import qualified Libsodium as Na
+
+import qualified Crypto.Sign as Sign
+
+
+seedSize :: R.Range Int
+seedSize = R.singleton $ fromIntegral Na.crypto_sign_seedbytes
+
+
+hprop_encode_decode_seed :: Property
+hprop_encode_decode_seed = property $ do
+    seed <- evalMaybe . sizedByteArray =<< forAll (G.bytes seedSize)
+    (pk, sk) <- forAllT . liftIO $ Sign.keypairFromSeed seed
+    msg <- forAll $ G.bytes (R.linear 0 1_000)
+    tripping msg (encodeBs sk) (decodeBs pk)
+  where
+    -- We need to specify the type of the signed msg as it is polymorphic
+    encodeBs sk msg = Sign.create sk msg :: ByteString
+    decodeBs pk ct = Sign.open pk ct :: Maybe ByteString
