diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# ChangeLog for hpke
+
+## 0.0.0
+
+* Initial release
diff --git a/Crypto/HPKE.hs b/Crypto/HPKE.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE.hs
@@ -0,0 +1,71 @@
+-- | Hybrid Public Key Encryption (RFC9180).
+module Crypto.HPKE (
+    -- * IDs
+    KEM_ID (..),
+    KDF_ID (..),
+    AEAD_ID (..),
+
+    -- * Setup
+
+    -- ** For mode_base and mode_auth
+    setupBaseS,
+    setupBaseR,
+
+    -- ** For mode_psk and mode_auth_psk
+    setupPSKS,
+    setupPSKR,
+
+    -- * Encryption and Decyption
+    seal,
+    open,
+
+    -- * Secret export
+    exportS,
+    exportR,
+
+    -- * Types
+    ContextS,
+    ContextR,
+    EncodedSecretKey (..),
+    EncodedPublicKey (..),
+    SharedSecret (..),
+    Info,
+    PSK,
+    PSK_ID,
+    AAD,
+    PlainText,
+    CipherText,
+    Key,
+
+    -- * Error
+    HPKEError (..),
+
+    -- * Misc
+    nEnc,
+    nTag,
+) where
+
+import Crypto.HPKE.Context
+import Crypto.HPKE.ID
+import Crypto.HPKE.Setup
+import Crypto.HPKE.Types
+
+-- | Length of "enc", aka sender's public key.
+{- FOURMOLU_DISABLE -}
+nEnc :: KEM_ID -> Int
+nEnc DHKEM_P256_HKDF_SHA256   =  65
+nEnc DHKEM_P384_HKDF_SHA384   =  97
+nEnc DHKEM_P521_HKDF_SHA512   = 133
+nEnc DHKEM_X25519_HKDF_SHA256 =  32
+nEnc DHKEM_X448_HKDF_SHA512   =  56
+nEnc _                        =  0
+{- FOURMOLU_ENABLE -}
+
+-- | Length of AEAD tag.
+{- FOURMOLU_DISABLE -}
+nTag :: AEAD_ID -> Int
+nTag AES_128_GCM      = 16
+nTag AES_256_GCM      = 16
+nTag ChaCha20Poly1305 = 16
+nTag _                =  0
+{- FOURMOLU_ENABLE -}
diff --git a/Crypto/HPKE/AEAD.hs b/Crypto/HPKE/AEAD.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/AEAD.hs
@@ -0,0 +1,193 @@
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+module Crypto.HPKE.AEAD (
+    Aead (..),
+) where
+
+import Crypto.Cipher.AES (AES128, AES256)
+import qualified Crypto.Cipher.ChaChaPoly1305 as CCP
+import Crypto.Cipher.Types (AEAD (..), AuthTag (..), BlockCipher)
+import qualified Crypto.Cipher.Types as Cipher
+import Data.ByteArray (ByteArray, ByteArrayAccess)
+import qualified Data.ByteString as BS
+import Data.Tuple (swap)
+
+import Crypto.HPKE.Types
+
+-- $setup
+-- >>> :set -XOverloadedStrings
+-- >>> import Data.ByteString
+
+----------------------------------------------------------------
+
+class Aead a where
+    sealA :: Proxy a -> Key -> Seal
+    openA :: Proxy a -> Key -> Open
+    nK :: Proxy a -> Int
+    nN :: Proxy a -> Int
+    nT :: Proxy a -> Int
+
+mkSealA :: AeadEncrypt -> p -> Key -> Seal
+mkSealA enc _ key nonce aad plain = do
+    (cipher, AuthTag tag) <- enc key nonce aad plain
+    return (cipher <> convert tag)
+
+mkOpenA :: AeadDecrypt -> Int -> p -> Key -> Open
+mkOpenA dec len _ key nonce aad cipher = do
+    (plain, AuthTag tag) <- dec key nonce aad cipher'
+    if tag == convert tag'
+        then Right plain
+        else Left $ OpenError "tag mismatch"
+  where
+    brkpt = BS.length cipher - len
+    (cipher', tag') = BS.splitAt brkpt cipher
+
+----------------------------------------------------------------
+
+-- 'forall' is necessary because of 'type'
+type AeadEncrypt =
+    forall k n a t
+     . ( ByteArray k
+       , ByteArrayAccess n
+       , ByteArrayAccess a
+       , ByteArray t
+       )
+    => k -> n -> a -> t -> Either HPKEError (t, AuthTag)
+
+type AeadDecrypt =
+    forall k n a t
+     . ( ByteArray k
+       , ByteArrayAccess n
+       , ByteArrayAccess a
+       , ByteArray t
+       )
+    => k -> n -> a -> t -> Either HPKEError (t, AuthTag)
+
+----------------------------------------------------------------
+
+initAES
+    :: ( ByteArray k
+       , ByteArrayAccess n
+       , BlockCipher c
+       )
+    => k -> n -> Maybe (AEAD c)
+initAES key nonce = case mst of
+    CryptoPassed st -> Just st
+    CryptoFailed _ -> Nothing
+  where
+    mst = do
+        st0 <- Cipher.cipherInit key
+        Cipher.aeadInit Cipher.AEAD_GCM st0 nonce
+
+----------------------------------------------------------------
+
+-- | From RFC 9180 A.1
+--
+-- >>> let key = "\x45\x31\x68\x5d\x41\xd6\x5f\x03\xdc\x48\xf6\xb8\x30\x2c\x05\xb0" :: ByteString
+-- >>> let nonce = "\x56\xd8\x90\xe5\xac\xca\xaf\x01\x1c\xff\x4b\x7d" :: ByteString
+-- >>> let aad = "\x43\x6f\x75\x6e\x74\x2d\x30" :: ByteString
+-- >>> let plain = "The quick brown fox jumps over the very lazy dog." :: ByteString
+-- >>> let proxy = Proxy :: Proxy AES128
+-- >>> sealA proxy key nonce aad plain >>= openA proxy key nonce aad
+-- Right "The quick brown fox jumps over the very lazy dog."
+instance Aead AES128 where
+    sealA = mkSealA encryptAes128gcm
+    openA = mkOpenA decryptAes128gcm aes128tagLength
+    nK = const 16
+    nN = const 12
+    nT = const 16
+
+encryptAes128gcm :: AeadEncrypt
+encryptAes128gcm key nonce aad plain = case initAES key nonce of
+    Nothing -> Left $ SealError "encryptAes128gcm"
+    Just st -> Right $ simpleEncrypt (st :: AEAD AES128) aad plain aes128tagLength
+
+decryptAes128gcm :: AeadDecrypt
+decryptAes128gcm key nonce aad cipher = case initAES key nonce of
+    Nothing -> Left $ OpenError "decrypttAes128gcm"
+    Just st -> Right $ simpleDecrypt (st :: AEAD AES128) aad cipher aes128tagLength
+
+aes128tagLength :: Int
+aes128tagLength = 16
+
+----------------------------------------------------------------
+
+-- | From RFC 9180 A.6
+--
+-- >>> let key = "\x75\x1e\x34\x6c\xe8\xf0\xdd\xb2\x30\x5c\x8a\x2a\x85\xc7\x0d\x5c\xf5\x59\xc5\x30\x93\x65\x6b\xe6\x36\xb9\x40\x6d\x4d\x7d\x1b\x70" :: ByteString
+-- >>> let nonce = "\x55\xff\x7a\x7d\x73\x9c\x69\xf4\x4b\x25\x44\x7b" :: ByteString
+-- >>> let aad = "\x43\x6f\x75\x6e\x74\x2d\x30" :: ByteString
+-- >>> let plain = "The quick brown fox jumps over the very lazy dog." :: ByteString
+-- >>> let proxy = Proxy :: Proxy AES256
+-- >>> sealA proxy key nonce aad plain >>= openA proxy key nonce aad
+-- Right "The quick brown fox jumps over the very lazy dog."
+instance Aead AES256 where
+    sealA = mkSealA encryptAes256gcm
+    openA = mkOpenA decryptAes256gcm aes256tagLength
+    nK = const 32
+    nN = const 12
+    nT = const 16
+
+encryptAes256gcm :: AeadEncrypt
+encryptAes256gcm key nonce aad plain = case initAES key nonce of
+    Nothing -> Left $ SealError "encryptAes256gcm"
+    Just st -> Right $ simpleEncrypt (st :: AEAD AES256) aad plain aes256tagLength
+
+decryptAes256gcm :: AeadDecrypt
+decryptAes256gcm key nonce aad cipher = case initAES key nonce of
+    Nothing -> Left $ OpenError "decryptAes256gcm"
+    Just st -> Right $ simpleDecrypt (st :: AEAD AES256) aad cipher aes256tagLength
+
+aes256tagLength :: Int
+aes256tagLength = 16
+
+----------------------------------------------------------------
+
+-- | From RFC 9180 A.5
+--
+-- >>> let key = "\xa8\xf4\x54\x90\xa9\x2a\x3b\x04\xd1\xdb\xf6\xcf\x2c\x39\x39\xad\x8b\xfc\x9b\xfc\xb9\x7c\x04\xbf\xfe\x11\x67\x30\xc9\xdf\xe3\xfc" :: ByteString
+-- >>> let nonce = "\x72\x6b\x43\x90\xed\x22\x09\x80\x9f\x58\xc6\x93" :: ByteString
+-- >>> let aad = "\x43\x6f\x75\x6e\x74\x2d\x30" :: ByteString
+-- >>> let plain = "The quick brown fox jumps over the very lazy dog." :: ByteString
+-- >>> let proxy = Proxy :: Proxy CCP.ChaCha20Poly1305
+-- >>> sealA proxy key nonce aad plain >>= openA proxy key nonce aad
+-- Right "The quick brown fox jumps over the very lazy dog."
+instance Aead CCP.ChaCha20Poly1305 where
+    sealA = mkSealA encryptChacha20poly1305
+    openA = mkOpenA decryptChacha20poly1305 chacha20poly1305tagLength
+    nK = const 32
+    nN = const 12
+    nT = const 16
+
+encryptChacha20poly1305 :: AeadEncrypt
+encryptChacha20poly1305 key nonce aad plain =
+    case CCP.aeadChacha20poly1305Init key nonce of
+        CryptoPassed st -> Right $ simpleEncrypt st aad plain chacha20poly1305tagLength
+        CryptoFailed _ -> Left $ SealError "encryptChacha20poly1305"
+
+decryptChacha20poly1305 :: AeadDecrypt
+decryptChacha20poly1305 key nonce aad cipher =
+    case CCP.aeadChacha20poly1305Init key nonce of
+        CryptoPassed st -> Right $ simpleDecrypt st aad cipher chacha20poly1305tagLength
+        CryptoFailed _ -> Left $ SealError "decryptChacha20poly1305"
+
+chacha20poly1305tagLength :: Int
+chacha20poly1305tagLength = 16
+
+----------------------------------------------------------------
+
+simpleEncrypt
+    :: (ByteArrayAccess a, ByteArray t)
+    => AEAD cipher -> a -> t -> Int -> (t, AuthTag)
+simpleEncrypt st aad plain taglen =
+    swap $ Cipher.aeadSimpleEncrypt st aad plain taglen
+
+simpleDecrypt
+    :: (ByteArrayAccess a, ByteArray t)
+    => AEAD cipher -> a -> t -> Int -> (t, AuthTag)
+simpleDecrypt st aad cipher taglen = (plain, tag)
+  where
+    st2 = Cipher.aeadAppendHeader st aad
+    (plain, st3) = Cipher.aeadDecrypt st2 cipher
+    tag = Cipher.aeadFinalize st3 taglen
diff --git a/Crypto/HPKE/Context.hs b/Crypto/HPKE/Context.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/Context.hs
@@ -0,0 +1,122 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Crypto.HPKE.Context (
+    -- * Sender
+    ContextS,
+    newContextS,
+    seal,
+    exportS,
+
+    -- * Receiver
+    ContextR,
+    newContextR,
+    open,
+    exportR,
+) where
+
+import qualified Control.Exception as E
+import Data.ByteArray (xor)
+import qualified Data.ByteString as BS
+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
+
+import Crypto.HPKE.Types
+
+----------------------------------------------------------------
+
+-- | Context for senders.
+data ContextS = ContextS
+    { seqRefS :: IORef Integer
+    , sealS :: Seal
+    , nonceBaseS :: Nonce
+    , expandS :: Info -> Int -> Key
+    }
+
+-- | Context for receivers.
+data ContextR = ContextR
+    { seqRefR :: IORef Integer
+    , openR :: Open
+    , nonceBaseR :: Nonce
+    , expandR :: Info -> Int -> Key
+    }
+
+----------------------------------------------------------------
+
+-- | Encryption.
+--   This throws 'HPKEError'.
+seal :: ContextS -> AAD -> PlainText -> IO CipherText
+seal ContextS{..} aad pt = do
+    seqI <- readIORef seqRefS
+    let len = BS.length nonceBaseS
+        seqBS = i2ospOf_ len seqI :: ByteString
+        nonce = seqBS `xor` nonceBaseS
+        ect = sealS nonce aad pt
+        seqI' = seqI + 1
+    writeIORef seqRefS seqI'
+    case ect of
+        Right ct -> return ct
+        Left err -> E.throwIO err
+
+-- | Decryption.
+--   This throws 'HPKEError'.
+open
+    :: ContextR -> AAD -> CipherText -> IO PlainText
+open ContextR{..} aad ct = do
+    seqI <- readIORef seqRefR
+    let len = BS.length nonceBaseR
+        seqBS = i2ospOf_ len seqI :: ByteString
+        nonce = seqBS `xor` nonceBaseR
+        ept = openR nonce aad ct
+    case ept of
+        Left err -> E.throwIO err
+        Right pt -> do
+            let seqI' = seqI + 1
+            writeIORef seqRefR seqI'
+            return pt
+
+----------------------------------------------------------------
+
+-- | Exporting secret.
+exportS :: ContextS -> Info -> Int -> Key
+exportS ContextS{..} exporter_context len =
+    expandS exporter_context len
+
+-- | Exporting secret.
+exportR :: ContextR -> Info -> Int -> Key
+exportR ContextR{..} exporter_context len =
+    expandR exporter_context len
+
+----------------------------------------------------------------
+
+newContextS
+    :: Key
+    -> Nonce
+    -> (Key -> Seal)
+    -> (Info -> Int -> Key)
+    -> IO ContextS
+newContextS key nonce_base seal' expand = do
+    seqref <- newIORef 0
+    return $
+        ContextS
+            { seqRefS = seqref
+            , sealS = seal' key
+            , nonceBaseS = nonce_base
+            , expandS = expand
+            }
+
+----------------------------------------------------------------
+
+newContextR
+    :: Key
+    -> Nonce
+    -> (Key -> Open)
+    -> (Info -> Int -> Key)
+    -> IO ContextR
+newContextR key nonce_base open' expand = do
+    seqref <- newIORef 0
+    return $
+        ContextR
+            { seqRefR = seqref
+            , openR = open' key
+            , nonceBaseR = nonce_base
+            , expandR = expand
+            }
diff --git a/Crypto/HPKE/ID.hs b/Crypto/HPKE/ID.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/ID.hs
@@ -0,0 +1,183 @@
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE PatternSynonyms #-}
+
+module Crypto.HPKE.ID (
+    AEAD_ID (AES_128_GCM, AES_256_GCM, ChaCha20Poly1305, ..),
+    AEADCipher (..),
+    defaultAEADMap,
+    --
+    KDF_ID (HKDF_SHA256, HKDF_SHA384, HKDF_SHA512, ..),
+    KDFHash (..),
+    defaultKDFMap,
+    --
+    KEM_ID (
+        DHKEM_P256_HKDF_SHA256,
+        DHKEM_P384_HKDF_SHA384,
+        DHKEM_P521_HKDF_SHA512,
+        DHKEM_X25519_HKDF_SHA256,
+        DHKEM_X448_HKDF_SHA512,
+        ..
+    ),
+    defaultKEMMap,
+    KEMGroup (..),
+    --
+    HPKEMap (..),
+    defaultHPKEMap,
+) where
+
+import Crypto.Cipher.AES (AES128, AES256)
+import Crypto.Cipher.ChaChaPoly1305 (ChaCha20Poly1305)
+import Crypto.ECC (
+    Curve_P256R1,
+    Curve_P384R1,
+    Curve_P521R1,
+    Curve_X25519,
+    Curve_X448,
+    EllipticCurve (..),
+    EllipticCurveDH (..),
+ )
+import Data.Proxy (Proxy (..))
+import Data.Word (Word16)
+import Text.Printf (printf)
+
+import Crypto.HPKE.AEAD
+import Crypto.HPKE.KDF
+
+----------------------------------------------------------------
+
+-- | ID for authenticated encryption with additional data
+newtype AEAD_ID = AEAD_ID {fromAEAD_ID :: Word16} deriving (Eq)
+
+{- FOURMOLU_DISABLE -}
+pattern AES_128_GCM      :: AEAD_ID
+pattern AES_128_GCM       = AEAD_ID 0x0001
+pattern AES_256_GCM      :: AEAD_ID
+pattern AES_256_GCM       = AEAD_ID 0x0002
+pattern ChaCha20Poly1305 :: AEAD_ID
+pattern ChaCha20Poly1305  = AEAD_ID 0x0003
+
+instance Show AEAD_ID where
+    show AES_128_GCM      = "AES_128_GCM"
+    show AES_256_GCM      = "AES_256_GCM"
+    show ChaCha20Poly1305 = "ChaCha20Poly1305"
+    show (AEAD_ID n)      = "AEAD_ID 0x" ++ printf "%04x" n
+{- FOURMOLU_ENABLE -}
+
+{- FOURMOLU_DISABLE -}
+aes128 :: Proxy AES128
+aes128  = Proxy :: Proxy AES128
+aes256 :: Proxy AES256
+aes256  = Proxy :: Proxy AES256
+chacha :: Proxy ChaCha20Poly1305
+chacha  = Proxy :: Proxy ChaCha20Poly1305
+{- FOURMOLU_ENABLE -}
+
+data AEADCipher = forall a. Aead a => AEADCipher (Proxy a)
+
+{- FOURMOLU_DISABLE -}
+defaultAEADMap :: [(AEAD_ID, AEADCipher)]
+defaultAEADMap =
+    [ (AES_128_GCM,      AEADCipher aes128)
+    , (AES_256_GCM,      AEADCipher aes256)
+    , (ChaCha20Poly1305, AEADCipher chacha)
+    ]
+{- FOURMOLU_ENABLE -}
+
+----------------------------------------------------------------
+
+-- | ID for key derivation function.
+newtype KDF_ID = KDF_ID {fromKDF_ID :: Word16} deriving (Eq)
+
+{- FOURMOLU_DISABLE -}
+pattern HKDF_SHA256 :: KDF_ID
+pattern HKDF_SHA256  = KDF_ID 0x0001
+pattern HKDF_SHA384 :: KDF_ID
+pattern HKDF_SHA384  = KDF_ID 0x0002
+pattern HKDF_SHA512 :: KDF_ID
+pattern HKDF_SHA512  = KDF_ID 0x0003
+
+instance Show KDF_ID where
+    show HKDF_SHA256 = "HKDF_SHA256"
+    show HKDF_SHA384 = "HKDF_SHA384"
+    show HKDF_SHA512 = "HKDF_SHA512"
+    show (KDF_ID n)  = "HKDF_ID 0x" ++ printf "%04x" n
+{- FOURMOLU_ENABLE -}
+
+data KDFHash = forall h. (HashAlgorithm h, KDF h) => KDFHash h
+
+defaultKDFMap :: [(KDF_ID, KDFHash)]
+defaultKDFMap =
+    [ (HKDF_SHA256, KDFHash SHA256)
+    , (HKDF_SHA384, KDFHash SHA384)
+    , (HKDF_SHA512, KDFHash SHA512)
+    ]
+
+----------------------------------------------------------------
+----------------------------------------------------------------
+
+-- | ID for key encapsulation mechanism.
+newtype KEM_ID = KEM_ID {fromKEM_ID :: Word16} deriving (Eq)
+
+{- FOURMOLU_DISABLE -}
+pattern DHKEM_P256_HKDF_SHA256   :: KEM_ID
+pattern DHKEM_P256_HKDF_SHA256    = KEM_ID 0x0010
+pattern DHKEM_P384_HKDF_SHA384   :: KEM_ID
+pattern DHKEM_P384_HKDF_SHA384    = KEM_ID 0x0011
+pattern DHKEM_P521_HKDF_SHA512   :: KEM_ID
+pattern DHKEM_P521_HKDF_SHA512    = KEM_ID 0x0012
+pattern DHKEM_X25519_HKDF_SHA256 :: KEM_ID
+pattern DHKEM_X25519_HKDF_SHA256  = KEM_ID 0x0020
+pattern DHKEM_X448_HKDF_SHA512   :: KEM_ID
+pattern DHKEM_X448_HKDF_SHA512    = KEM_ID 0x0021
+
+instance Show KEM_ID where
+    show DHKEM_P256_HKDF_SHA256   = "DHKEM(P-256, HKDF-SHA256)"
+    show DHKEM_P384_HKDF_SHA384   = "DHKEM(P-384, HKDF-SHA384)"
+    show DHKEM_P521_HKDF_SHA512   = "DHKEM(P-521, HKDF-SHA512)"
+    show DHKEM_X25519_HKDF_SHA256 = "DHKEM(X25519, HKDF-SHA256)"
+    show DHKEM_X448_HKDF_SHA512   = "DHKEM(X448, HKDF-SHA512)"
+    show (KEM_ID n)               = "DHKEM_ID 0x" ++ printf "%04x" n
+{- FOURMOLU_ENABLE -}
+
+----------------------------------------------------------------
+
+{- FOURMOLU_DISABLE -}
+p256   :: Proxy Curve_P256R1
+p256    = Proxy :: Proxy Curve_P256R1
+p384   :: Proxy Curve_P384R1
+p384    = Proxy :: Proxy Curve_P384R1
+p521   :: Proxy Curve_P521R1
+p521    = Proxy :: Proxy Curve_P521R1
+x25519 :: Proxy Curve_X25519
+x25519  = Proxy :: Proxy Curve_X25519
+x448   :: Proxy Curve_X448
+x448    = Proxy :: Proxy Curve_X448
+
+data KEMGroup
+    = forall c. (EllipticCurve c, EllipticCurveDH c) => KEMGroup (Proxy c)
+
+defaultKEMMap :: [(KEM_ID, (KEMGroup, KDFHash))]
+defaultKEMMap =
+    [ (DHKEM_P256_HKDF_SHA256,   (KEMGroup p256,   KDFHash SHA256))
+    , (DHKEM_P384_HKDF_SHA384,   (KEMGroup p384,   KDFHash SHA384))
+    , (DHKEM_P521_HKDF_SHA512,   (KEMGroup p521,   KDFHash SHA512))
+    , (DHKEM_X25519_HKDF_SHA256, (KEMGroup x25519, KDFHash SHA256))
+    , (DHKEM_X448_HKDF_SHA512,   (KEMGroup x448,   KDFHash SHA512))
+    ]
+{- FOURMOLU_ENABLE -}
+
+----------------------------------------------------------------
+
+data HPKEMap = HPKEMap
+    { kemMap :: [(KEM_ID, (KEMGroup, KDFHash))]
+    , kdfMap :: [(KDF_ID, KDFHash)]
+    , cipherMap :: [(AEAD_ID, AEADCipher)]
+    }
+
+defaultHPKEMap :: HPKEMap
+defaultHPKEMap =
+    HPKEMap
+        { kemMap = defaultKEMMap
+        , kdfMap = defaultKDFMap
+        , cipherMap = defaultAEADMap
+        }
diff --git a/Crypto/HPKE/Internal.hs b/Crypto/HPKE/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/Internal.hs
@@ -0,0 +1,40 @@
+module Crypto.HPKE.Internal (
+    -- * Extensible map
+    HPKEMap (..),
+    defaultHPKEMap,
+    setupS,
+    setupR,
+
+    -- * Unified types
+    KEMGroup (..),
+    KDFHash (..),
+    AEADCipher (..),
+
+    -- * API
+    Aead (..),
+    KDF (..),
+
+    -- * Types
+    Mode (..),
+    PublicKey,
+    SecretKey,
+    Seal,
+    Open,
+    Nonce,
+    Suite,
+    Salt,
+    Label,
+    IKM,
+
+    -- * Generating key pair
+    genKeyPair,
+) where
+
+import Crypto.HPKE.AEAD
+import Crypto.HPKE.ID
+import Crypto.HPKE.KDF
+import Crypto.HPKE.KeyPair
+import Crypto.HPKE.KeySchedule
+import Crypto.HPKE.PublicKey
+import Crypto.HPKE.Setup
+import Crypto.HPKE.Types
diff --git a/Crypto/HPKE/KDF.hs b/Crypto/HPKE/KDF.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/KDF.hs
@@ -0,0 +1,71 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Crypto.HPKE.KDF (
+    KDF (..),
+    HashAlgorithm,
+    SHA256 (..),
+    SHA384 (..),
+    SHA512 (..),
+    PRK,
+    extractAndExpand,
+)
+where
+
+import Crypto.Hash.Algorithms (
+    HashAlgorithm,
+    SHA256 (..),
+    SHA384 (..),
+    SHA512 (..),
+ )
+import Crypto.KDF.HKDF (PRK)
+import qualified Crypto.KDF.HKDF as HKDF
+
+import Crypto.HPKE.Types
+
+----------------------------------------------------------------
+
+class KDF h where
+    labeledExtract :: Suite -> Salt -> Label -> IKM -> PRK h
+    labeledExpand :: Suite -> PRK h -> Label -> Info -> Int -> Key
+
+instance KDF SHA256 where
+    labeledExtract = labeledExtract_
+    labeledExpand = labeledExpand_
+
+instance KDF SHA384 where
+    labeledExtract = labeledExtract_
+    labeledExpand = labeledExpand_
+
+instance KDF SHA512 where
+    labeledExtract = labeledExtract_
+    labeledExpand = labeledExpand_
+
+----------------------------------------------------------------
+
+labeledExtract_
+    :: HashAlgorithm a => Suite -> Salt -> Label -> IKM -> PRK a
+labeledExtract_ suite salt label ikm = HKDF.extract salt labeled_ikm
+  where
+    labeled_ikm = "HPKE-v1" <> suite <> label <> ikm
+
+labeledExpand_
+    :: HashAlgorithm a => Suite -> PRK a -> Label -> Info -> Int -> Key
+labeledExpand_ suite prk label info len = HKDF.expand prk labeled_info len
+  where
+    labeled_info =
+        i2ospOf_ 2 (fromIntegral len) <> "HPKE-v1" <> suite <> label <> info
+
+----------------------------------------------------------------
+
+extractAndExpand
+    :: forall h
+     . (HashAlgorithm h, KDF h)
+    => h -> Suite -> KeyDeriveFunction
+extractAndExpand h suite dh kem_context = shared_secret
+  where
+    eae_prk :: PRK h
+    eae_prk = labeledExtract suite "" "eae_prk" $ convert dh
+    siz = hashDigestSize h
+    shared_secret =
+        labeledExpand suite eae_prk "shared_secret" kem_context siz
diff --git a/Crypto/HPKE/KEM.hs b/Crypto/HPKE/KEM.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/KEM.hs
@@ -0,0 +1,185 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Crypto.HPKE.KEM (
+    encapGen,
+    encapEnv,
+    decapEnv,
+    genKeyPairP,
+)
+where
+
+import qualified Control.Exception as E
+import Crypto.ECC (
+    EllipticCurve (..),
+    EllipticCurveDH (..),
+    KeyPair (..),
+ )
+import Crypto.Random (drgNew, withDRG)
+
+import Crypto.HPKE.PublicKey
+import Crypto.HPKE.Types
+
+----------------------------------------------------------------
+
+encap
+    :: (EllipticCurve group, EllipticCurveDH group)
+    => Env group
+    -> Encap
+encap Env{..} enc0@(EncodedPublicKey pkRm) = do
+    pkR <- deserializePublicKey envProxy enc0
+    let skE = envSecretKey
+    dh0 <- ecdh' envProxy skE pkR $ EncapError "encap"
+    (dh, pkSm) <- case envAuthKey of
+        Nothing -> return (dh0, "")
+        Just skS -> do
+            let pkS = scalarToPoint envProxy skS
+            dh1 <- ecdh' envProxy skS pkR $ EncapError "encap"
+            let EncodedPublicKey pk = serializePublicKey envProxy pkS
+            return (dh0 <> dh1, pk)
+    let pkE = scalarToPoint envProxy skE
+    let enc@(EncodedPublicKey pkEm) = serializePublicKey envProxy pkE
+        kem_context = pkEm <> pkRm <> pkSm
+        shared_secret = SharedSecret $ convert $ envDerive dh kem_context
+    return (shared_secret, enc)
+
+encapGen
+    :: (EllipticCurve group, EllipticCurveDH group)
+    => Proxy group
+    -> KeyDeriveFunction
+    -> Maybe EncodedSecretKey
+    -> IO Encap
+encapGen proxy derive mskSm = do
+    mskS <- case mskSm of
+        Nothing -> return $ Nothing
+        Just skSm -> case deserializeSecretKey proxy skSm of
+            Left err -> E.throwIO err
+            Right x -> return $ Just x
+    env <- genEnv proxy derive mskS
+    return $ encap env
+
+encapEnv
+    :: (EllipticCurve group, EllipticCurveDH group)
+    => Proxy group
+    -> KeyDeriveFunction
+    -> EncodedSecretKey
+    -> Maybe EncodedSecretKey
+    -> Encap
+encapEnv proxy derive skRm skSm enc = do
+    env <- newEnvDeserialize proxy derive skRm skSm
+    encap env enc
+
+----------------------------------------------------------------
+
+decap
+    :: (EllipticCurve group, EllipticCurveDH group)
+    => Env group
+    -> Decap
+decap Env{..} enc@(EncodedPublicKey pkEm) = do
+    pkE <- deserializePublicKey envProxy enc
+    let skR = envSecretKey
+    dh0 <- ecdh' envProxy skR pkE $ DecapError "decap"
+    (dh, pkSm) <- case envAuthKey of
+        Nothing -> return (dh0, "")
+        Just skS -> do
+            let pkS = scalarToPoint envProxy skS
+            dh1 <- ecdh' envProxy skR pkS $ EncapError "decap"
+            let EncodedPublicKey pk = serializePublicKey envProxy pkS
+            return (dh0 <> dh1, pk)
+
+    let pkR = scalarToPoint envProxy skR
+    let EncodedPublicKey pkRm = serializePublicKey envProxy pkR
+        kem_context = pkEm <> pkRm <> pkSm
+        shared_secret = SharedSecret $ convert $ envDerive dh kem_context
+    return shared_secret
+
+decapEnv
+    :: (EllipticCurve group, EllipticCurveDH group)
+    => Proxy group
+    -> KeyDeriveFunction
+    -> EncodedSecretKey
+    -> Maybe EncodedSecretKey
+    -> Decap
+decapEnv proxy derive skRm mskSm enc = do
+    env <- newEnvDeserialize proxy derive skRm mskSm
+    decap env enc
+
+----------------------------------------------------------------
+
+{- FOURMOLU_DISABLE -}
+data Env group = Env
+    { envSecretKey :: SecretKey group
+    , envAuthKey   :: Maybe (SecretKey group)
+    , envProxy     :: Proxy group
+    , envDerive    :: KeyDeriveFunction
+    }
+{- FOURMOLU_ENABLE -}
+
+----------------------------------------------------------------
+
+newEnv
+    :: forall group
+     . EllipticCurve group
+    => KeyDeriveFunction
+    -> SecretKey group
+    -> Maybe (SecretKey group)
+    -> Env group
+newEnv derive skR mskS =
+    Env
+        { envSecretKey = skR
+        , envAuthKey = mskS
+        , envProxy = proxy
+        , envDerive = derive
+        }
+  where
+    proxy = Proxy :: Proxy group
+
+----------------------------------------------------------------
+
+genEnv
+    :: EllipticCurve group
+    => Proxy group
+    -> KeyDeriveFunction
+    -> Maybe (SecretKey group)
+    -> IO (Env group)
+genEnv proxy derive mskS = do
+    (_, sk) <- genKeyPairP proxy
+    return $ newEnv derive sk mskS
+
+genKeyPairP
+    :: EllipticCurve curve
+    => proxy curve -> IO (Point curve, Scalar curve)
+genKeyPairP proxy = do
+    gen <- drgNew
+    let (KeyPair pk sk, _) = withDRG gen $ curveGenerateKeyPair proxy
+    return (pk, sk)
+
+----------------------------------------------------------------
+
+newEnvDeserialize
+    :: EllipticCurve group
+    => Proxy group
+    -> KeyDeriveFunction
+    -> EncodedSecretKey
+    -> Maybe EncodedSecretKey
+    -> Either HPKEError (Env group)
+newEnvDeserialize proxy derive skRm mskSm = do
+    skR <- deserializeSecretKey proxy skRm
+    mskS <- case mskSm of
+        Nothing -> Right $ Nothing
+        Just skSm -> Just <$> deserializeSecretKey proxy skSm
+    return $ newEnv derive skR mskS
+
+----------------------------------------------------------------
+
+ecdh'
+    :: EllipticCurveDH group
+    => Proxy group
+    -> SecretKey group
+    -> PublicKey group
+    -> a
+    -> Either a SharedSecret
+ecdh' proxy sk pk err = case ecdh proxy sk pk of
+    CryptoPassed a -> Right a
+    CryptoFailed _ -> Left err
diff --git a/Crypto/HPKE/KeyPair.hs b/Crypto/HPKE/KeyPair.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/KeyPair.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Crypto.HPKE.KeyPair where
+
+import qualified Control.Exception as E
+import Crypto.ECC (
+    EllipticCurve (..),
+ )
+import Crypto.HPKE.ID
+import Crypto.HPKE.KEM (genKeyPairP)
+import Crypto.HPKE.Types
+
+----------------------------------------------------------------
+
+-- | Generating a pair of public key and secret key based on
+-- 'KEM_ID'.
+genKeyPair
+    :: HPKEMap -> KEM_ID -> IO (EncodedPublicKey, EncodedSecretKey)
+genKeyPair HPKEMap{..} kem_id = case lookup kem_id kemMap of
+    Nothing -> E.throwIO $ Unsupported $ show kem_id
+    Just (KEMGroup proxy, _) -> do
+        (pk, sk) <- genKeyPairP proxy
+        let pkm = EncodedPublicKey $ encodePoint proxy pk
+            skm = EncodedSecretKey $ encodeScalar proxy sk
+        return (pkm, skm)
diff --git a/Crypto/HPKE/KeySchedule.hs b/Crypto/HPKE/KeySchedule.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/KeySchedule.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Crypto.HPKE.KeySchedule (
+    -- * Types
+    Mode (..),
+
+    -- * Key schedule
+    keySchedule,
+) where
+
+import Crypto.KDF.HKDF (toPRK)
+import qualified Data.ByteString as BS
+
+import Crypto.HPKE.KDF
+import Crypto.HPKE.Types
+
+----------------------------------------------------------------
+
+data Mode
+    = ModeBase
+    | ModePsk
+    | ModeAuth
+    | ModeAuthPsk
+    deriving (Eq, Show)
+
+{- FOURMOLU_DISABLE -}
+fromMode :: Mode -> Word8
+fromMode ModeBase    = 0x00
+fromMode ModePsk     = 0x01
+fromMode ModeAuth    = 0x02
+fromMode ModeAuthPsk = 0x03
+{- FOURMOLU_ENABLE -}
+
+----------------------------------------------------------------
+
+keySchedule
+    :: forall h
+     . (HashAlgorithm h, KDF h)
+    => h
+    -> Suite
+    -> Int
+    -> Int
+    -> Mode
+    -> Info
+    -> PSK
+    -> PSK_ID
+    -> SharedSecret
+    -> Either HPKEError (Key, Nonce, Int, PRK h)
+keySchedule h suite nk nn mode info psk psk_id shared_secret =
+    case toPRK exporter_secret of
+        Nothing -> Left $ KeyScheduleError "cannot convert to PRK"
+        Just prk -> Right (key, base_nonce, 0, prk)
+  where
+    psk_id_hash = labeledExtract suite "" "psk_id_hash" psk_id :: PRK h
+    info_hash = labeledExtract suite "" "info_hash" info :: PRK h
+    key_schedule_context =
+        BS.singleton (fromMode mode) <> convert psk_id_hash <> convert info_hash
+            :: ByteString
+
+    secret = labeledExtract suite (convert shared_secret) "secret" psk :: PRK h
+
+    key = labeledExpand suite secret "key" key_schedule_context nk
+    base_nonce = labeledExpand suite secret "base_nonce" key_schedule_context nn
+
+    exporter_secret = labeledExpand suite secret "exp" key_schedule_context $ hashDigestSize h
diff --git a/Crypto/HPKE/PublicKey.hs b/Crypto/HPKE/PublicKey.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/PublicKey.hs
@@ -0,0 +1,60 @@
+module Crypto.HPKE.PublicKey (
+    SecretKey,
+    PublicKey,
+    serializePublicKey,
+    deserializePublicKey,
+    serializeSecretKey,
+    deserializeSecretKey,
+)
+where
+
+import Crypto.ECC (
+    EllipticCurve (..),
+    Point,
+    Scalar,
+    decodePoint,
+    decodeScalar,
+    encodePoint,
+    encodeScalar,
+ )
+
+import Crypto.HPKE.Types
+
+-- $setup
+-- >>> :set -XOverloadedStrings
+-- >>> import Crypto.ECC
+-- >>> import Crypto.Hash.Algorithms
+-- >>> import Data.ByteString
+
+----------------------------------------------------------------
+
+type PublicKey group = Point group
+type SecretKey group = Scalar group
+
+----------------------------------------------------------------
+
+serializePublicKey
+    :: EllipticCurve group
+    => Proxy group -> PublicKey group -> EncodedPublicKey
+serializePublicKey proxy pk = EncodedPublicKey $ encodePoint proxy pk
+
+deserializePublicKey
+    :: EllipticCurve group
+    => Proxy group -> EncodedPublicKey -> Either HPKEError (PublicKey group)
+deserializePublicKey proxy (EncodedPublicKey pkm) =
+    case decodePoint proxy pkm of
+        CryptoPassed a -> Right a
+        CryptoFailed _ -> Left $ DeserializeError "deserializePublicKey"
+
+serializeSecretKey
+    :: EllipticCurve group
+    => Proxy group -> SecretKey group -> EncodedSecretKey
+serializeSecretKey proxy pk = EncodedSecretKey $ encodeScalar proxy pk
+
+deserializeSecretKey
+    :: EllipticCurve group
+    => Proxy group -> EncodedSecretKey -> Either HPKEError (SecretKey group)
+deserializeSecretKey proxy (EncodedSecretKey pkm) =
+    case decodeScalar proxy pkm of
+        CryptoPassed a -> Right a
+        CryptoFailed _ -> Left $ DeserializeError "deserializeSecretKey"
diff --git a/Crypto/HPKE/Setup.hs b/Crypto/HPKE/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/Setup.hs
@@ -0,0 +1,233 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Crypto.HPKE.Setup (
+    setupBaseS,
+    setupBaseR,
+    setupPSKS,
+    setupPSKR,
+    setupS,
+    setupR,
+) where
+
+import qualified Control.Exception as E
+
+import Crypto.HPKE.AEAD
+import Crypto.HPKE.Context
+import Crypto.HPKE.ID
+import Crypto.HPKE.KDF
+import Crypto.HPKE.KEM
+import Crypto.HPKE.KeySchedule
+import Crypto.HPKE.Types
+
+-- | Setting up base/auth mode for a sender.
+--   This throws 'HPKEError'.
+setupBaseS
+    :: KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> Maybe EncodedSecretKey
+    -- ^ My ephemeral secret key. Automatically generated if 'Nothing'
+    -> Maybe EncodedSecretKey
+    -- ^ My secret key for authentication.
+    --   'mode_base' is used if 'Nothing'. 'base_auth' is used, otherwise.
+    -> EncodedPublicKey
+    -- ^ Peer's public key.
+    -> Info
+    -> IO (EncodedPublicKey, ContextS)
+setupBaseS kem_id kdf_id aead_id mskEm mskSm pkRm info =
+    setupS defaultHPKEMap mode kem_id kdf_id aead_id mskEm mskSm pkRm info "" ""
+  where
+    mode = case mskSm of
+        Nothing -> ModeBase
+        _ -> ModeAuth
+
+-- | Setting up base/auth mode for a receiver with its key pair.
+--   This throws 'HPKEError'.
+setupBaseR
+    :: KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> EncodedSecretKey
+    -- ^ My secret key
+    -> Maybe EncodedSecretKey
+    -- ^ My secret key for authentication.
+    --   'mode_base' is used if 'Nothing'. 'base_auth' is used, otherwise.
+    -> EncodedPublicKey
+    -- ^ Peer's public key.
+    -> Info
+    -> IO ContextR
+setupBaseR kem_id kdf_id aead_id skRm mskSm enc info =
+    setupR defaultHPKEMap mode kem_id kdf_id aead_id skRm mskSm enc info "" ""
+  where
+    mode = case mskSm of
+        Nothing -> ModeBase
+        _ -> ModeAuth
+
+----------------------------------------------------------------
+
+-- | Setting up psk/auth_psk mode for a sender.
+--   This throws 'HPKEError'.
+setupPSKS
+    :: KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> Maybe EncodedSecretKey
+    -- ^ My ephemeral secret key. Automatically generated if 'Nothing'
+    -> Maybe EncodedSecretKey
+    -- ^ My secret key for authentication.
+    --   'mode_base' is used if 'Nothing'. 'base_auth' is used, otherwise.
+    -> EncodedPublicKey
+    -- ^ Peer's public key.
+    -> Info
+    -> PSK
+    -> PSK_ID
+    -> IO (EncodedPublicKey, ContextS)
+setupPSKS kem_id kdf_id aead_id skRm mskSm =
+    setupS defaultHPKEMap mode kem_id kdf_id aead_id skRm mskSm
+  where
+    mode = case mskSm of
+        Nothing -> ModePsk
+        _ -> ModeAuthPsk
+
+-- | Setting up psk/auth_psk mode for a receiver with its key pair.
+--   This throws 'HPKEError'.
+setupPSKR
+    :: KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> EncodedSecretKey
+    -- ^ My secret key
+    -> Maybe EncodedSecretKey
+    -- ^ My secret key for authentication.
+    --   'mode_base' is used if 'Nothing'. 'base_auth' is used, otherwise.
+    -> EncodedPublicKey
+    -- ^ Peer's public key.
+    -> Info
+    -> PSK
+    -> PSK_ID
+    -> IO ContextR
+setupPSKR kem_id kdf_id aead_id skRm mskSm =
+    setupR defaultHPKEMap mode kem_id kdf_id aead_id skRm mskSm
+  where
+    mode = case mskSm of
+        Nothing -> ModePsk
+        _ -> ModeAuthPsk
+
+----------------------------------------------------------------
+
+setupS
+    :: HPKEMap
+    -> Mode
+    -> KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> Maybe EncodedSecretKey
+    -- ^ My ephemeral secret key. Automatically generated if 'Nothing'
+    -> Maybe EncodedSecretKey
+    -- ^ My secret key for authentication.
+    --   'mode_base' is used if 'Nothing'. 'base_auth' is used, otherwise.
+    -> EncodedPublicKey
+    -- ^ Peer's public key.
+    -> Info
+    -> PSK
+    -> PSK_ID
+    -> IO (EncodedPublicKey, ContextS)
+setupS hpkeMap mode kem_id kdf_id aead_id mskEm mskSm pkRm info psk psk_id = do
+    verifyPSKInput mode psk psk_id
+    let r = look hpkeMap kem_id kdf_id aead_id
+    throwOnError r $ \((KEMGroup group, KDFHash h), KDFHash h', AEADCipher c) -> do
+        let derive = extractAndExpand h $ suiteKEM kem_id
+        encap <- case mskEm of
+            Nothing -> encapGen group derive mskSm
+            Just skEm -> return $ encapEnv group derive skEm mskSm
+        throwOnError (encap pkRm) $ \(shared_secret, enc) -> do
+            let (nk, nn, seal', _) = aeadParams c
+                suite' = suiteHPKE kem_id kdf_id aead_id
+                keys = keySchedule h' suite' nk nn mode info psk psk_id shared_secret
+            throwOnError keys $ \(key, nonce, _, prk) -> do
+                let expand' = labeledExpand suite' prk "sec"
+                ctx <- newContextS key nonce seal' expand'
+                return (enc, ctx)
+
+setupR
+    :: HPKEMap
+    -> Mode
+    -> KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> EncodedSecretKey
+    -- ^ My secret key
+    -> Maybe EncodedSecretKey
+    -- ^ My secret key for authentication.
+    --   'mode_base' is used if 'Nothing'. 'base_auth' is used, otherwise.
+    -> EncodedPublicKey
+    -- ^ Peer's public key.
+    -> Info
+    -> PSK
+    -> PSK_ID
+    -> IO ContextR
+setupR hpkeMap mode kem_id kdf_id aead_id skRm mskSm enc info psk psk_id = do
+    verifyPSKInput mode psk psk_id
+    let r = look hpkeMap kem_id kdf_id aead_id
+    throwOnError r $ \((KEMGroup group, KDFHash h), KDFHash h', AEADCipher c) -> do
+        let derive = extractAndExpand h $ suiteKEM kem_id
+            decap = decapEnv group derive skRm mskSm
+        throwOnError (decap enc) $ \shared_secret -> do
+            let (nk, nn, _, open') = aeadParams c
+                suite' = suiteHPKE kem_id kdf_id aead_id
+                keys = keySchedule h' suite' nk nn mode info psk psk_id shared_secret
+            throwOnError keys $ \(key, nonce, _, prk) -> do
+                let expand' = labeledExpand suite' prk "sec"
+                newContextR key nonce open' expand'
+
+aeadParams
+    :: Aead a
+    => Proxy a -> (Int, Int, Key -> Seal, Key -> Open)
+aeadParams c = (nK c, nN c, sealA c, openA c)
+
+throwOnError :: Either HPKEError v -> (v -> IO a) -> IO a
+throwOnError (Left err) _body = E.throwIO err
+throwOnError (Right ss) body = body ss
+
+----------------------------------------------------------------
+
+look
+    :: HPKEMap
+    -> KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> Either HPKEError ((KEMGroup, KDFHash), KDFHash, AEADCipher)
+look HPKEMap{..} kem_id kdf_id aead_id = do
+    k <- lookupE kem_id kemMap
+    h <- lookupE kdf_id kdfMap
+    a <- lookupE aead_id cipherMap
+    return (k, h, a)
+
+verifyPSKInput :: Mode -> PSK -> PSK_ID -> IO ()
+verifyPSKInput mode psk psk_id
+    | got_psk /= got_psk_id =
+        E.throwIO $ ValidationError "mismatch for psk and psk_id"
+    | got_psk && mode `elem` [ModeBase, ModeAuth] =
+        E.throwIO $ ValidationError "invalid mode (1)"
+    | (not got_psk) && mode `elem` [ModePsk, ModeAuthPsk] =
+        E.throwIO $ ValidationError "invalid mode (2)"
+    | otherwise = return ()
+  where
+    got_psk = psk /= ""
+    got_psk_id = psk_id /= ""
+
+----------------------------------------------------------------
+
+suiteKEM :: KEM_ID -> Suite
+suiteKEM kem_id = "KEM" <> i
+  where
+    i = i2ospOf_ 2 $ fromIntegral $ fromKEM_ID kem_id
+
+suiteHPKE :: KEM_ID -> KDF_ID -> AEAD_ID -> Suite
+suiteHPKE kem_id hkdf_id aead_id = "HPKE" <> i0 <> i1 <> i2
+  where
+    i0 = i2ospOf_ 2 $ fromIntegral $ fromKEM_ID kem_id
+    i1 = i2ospOf_ 2 $ fromIntegral $ fromKDF_ID hkdf_id
+    i2 = i2ospOf_ 2 $ fromIntegral $ fromAEAD_ID aead_id
diff --git a/Crypto/HPKE/Types.hs b/Crypto/HPKE/Types.hs
new file mode 100644
--- /dev/null
+++ b/Crypto/HPKE/Types.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+module Crypto.HPKE.Types (
+    HPKEError (..),
+    Salt,
+    IKM,
+    Key,
+    Suite,
+    Label,
+    KeyDeriveFunction,
+    Nonce,
+    AAD,
+    PlainText,
+    CipherText,
+    Seal,
+    Open,
+    EncodedPublicKey (..),
+    EncodedSecretKey (..),
+    Info,
+    PSK,
+    PSK_ID,
+    Encap,
+    Decap,
+    ExporterSecret,
+    -- rexport
+    CryptoFailable (..),
+    SharedSecret (..),
+    hashDigestSize,
+    i2ospOf_,
+    convert,
+    Proxy (..),
+    ByteString,
+    Word8,
+    Word16,
+    printf,
+    lookupE,
+) where
+
+import Control.Exception (Exception)
+import Crypto.ECC (SharedSecret (..))
+import Crypto.Error (CryptoFailable (..))
+import Crypto.Hash.IO (hashDigestSize)
+import Crypto.Number.Serialize (i2ospOf_)
+import Data.ByteArray (convert)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Base16 as B16
+import qualified Data.ByteString.Char8 as C8
+import Data.Proxy (Proxy (..))
+import Data.String
+import Data.Word (Word16, Word8)
+import Text.Printf (printf)
+
+----------------------------------------------------------------
+
+-- | Errors for HPKE
+data HPKEError
+    = ValidationError String
+    | DeserializeError String
+    | EncapError String
+    | DecapError String
+    | -- | Original
+      SealError String
+    | OpenError String
+    | MessageLimitReachedError String
+    | DeriveKeyPairError String
+    | -- | Original
+      KeyScheduleError String
+    | Unsupported String
+    deriving (Eq, Show)
+
+instance Exception HPKEError
+
+----------------------------------------------------------------
+
+-- | Encryption key.
+type Key = ByteString
+
+type Salt = ByteString
+type IKM = ByteString -- Input Keying Material
+type Suite = ByteString
+type Label = ByteString
+type KeyDeriveFunction = SharedSecret -> ByteString -> Key
+
+----------------------------------------------------------------
+
+type Nonce = ByteString
+
+-- | Additional authenticated data for AEAD.
+type AAD = ByteString
+
+-- | Plain text.
+type PlainText = ByteString
+
+-- | Cipher text (including a authentication tag)
+type CipherText = ByteString
+
+type Seal = Nonce -> AAD -> PlainText -> Either HPKEError CipherText
+type Open = Nonce -> AAD -> CipherText -> Either HPKEError PlainText
+
+----------------------------------------------------------------
+
+-- | Encoded public key.
+newtype EncodedPublicKey = EncodedPublicKey ByteString deriving (Eq)
+
+-- | Encoded secret key.
+newtype EncodedSecretKey = EncodedSecretKey ByteString deriving (Eq)
+
+instance Show EncodedPublicKey where
+    show (EncodedPublicKey pk) = showBS16 pk
+
+instance Show EncodedSecretKey where
+    show (EncodedSecretKey pk) = showBS16 pk
+
+instance IsString EncodedPublicKey where
+    fromString = EncodedPublicKey . fromString
+
+instance IsString EncodedSecretKey where
+    fromString = EncodedSecretKey . fromString
+
+showBS16 :: ByteString -> String
+showBS16 bs = "\"" <> s16 <> "\""
+  where
+    s16 = C8.unpack $ B16.encode bs
+
+----------------------------------------------------------------
+
+type Encap =
+    EncodedPublicKey -> Either HPKEError (SharedSecret, EncodedPublicKey)
+type Decap = EncodedPublicKey -> Either HPKEError SharedSecret
+
+----------------------------------------------------------------
+
+-- | Information string.
+type Info = ByteString
+
+-- | Pre-shared key.
+type PSK = ByteString
+
+-- | ID for pre-shared key.
+type PSK_ID = ByteString
+
+----------------------------------------------------------------
+
+lookupE :: (Eq k, Show k) => k -> [(k, v)] -> Either HPKEError v
+lookupE k table = case lookup k table of
+    Nothing -> Left $ Unsupported $ show k
+    Just v -> Right v
+
+----------------------------------------------------------------
+
+type ExporterSecret = ByteString
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2025, IIJ Innovation Institute Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+  * Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+  * Neither the name of the copyright holders nor the names of its
+    contributors may be used to endorse or promote products derived
+    from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/hpke.cabal b/hpke.cabal
new file mode 100644
--- /dev/null
+++ b/hpke.cabal
@@ -0,0 +1,62 @@
+cabal-version:      >=1.10
+name:               hpke
+version:            0.0.0
+license:            BSD3
+license-file:       LICENSE
+maintainer:         kazu@iij.ad.jp
+author:             Kazu Yamamoto
+synopsis:           Hybrid Public Key Encryption
+description:
+    Hybrid Public Key Encryption defined in RFC9180
+
+category:           Cryptography
+build-type:         Simple
+extra-source-files: ChangeLog.md
+
+library
+    exposed-modules:  Crypto.HPKE
+                      Crypto.HPKE.Internal
+    other-modules:    Crypto.HPKE.AEAD
+                      Crypto.HPKE.Context
+                      Crypto.HPKE.ID
+                      Crypto.HPKE.KDF
+                      Crypto.HPKE.KEM
+                      Crypto.HPKE.KeyPair
+                      Crypto.HPKE.KeySchedule
+                      Crypto.HPKE.PublicKey
+                      Crypto.HPKE.Setup
+                      Crypto.HPKE.Types
+    default-language: Haskell2010
+    ghc-options:      -Wall
+    build-depends:
+        base >=4.7 && <5,
+        base16-bytestring,
+        bytestring,
+        crypton >= 1.0.2,
+        memory
+
+    default-extensions: Strict StrictData
+
+test-suite spec
+    type:               exitcode-stdio-1.0
+    main-is:            Spec.hs
+    build-tool-depends: hspec-discover:hspec-discover
+    hs-source-dirs:     test
+    other-modules:      A1Spec
+                        A2Spec
+                        A3Spec
+                        A4Spec
+                        A5Spec
+                        A6Spec
+                        Test
+
+    default-language:   Haskell2010
+    default-extensions: Strict StrictData
+    ghc-options:        -Wall -threaded -rtsopts
+    build-depends:
+        base >=4.9 && <5,
+        QuickCheck,
+        bytestring,
+        base16-bytestring,
+        hpke,
+        hspec
diff --git a/test/A1Spec.hs b/test/A1Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/A1Spec.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module A1Spec where
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+import Data.ByteString ()
+import Test.Hspec
+
+import Test
+
+kem_id :: KEM_ID
+kem_id = DHKEM_X25519_HKDF_SHA256
+kdf_id :: KDF_ID
+kdf_id = HKDF_SHA256
+aead_id :: AEAD_ID
+aead_id = AES_128_GCM
+
+spec :: Spec
+spec = do
+    describe "A.1. DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM" $ do
+        it "A.1.1. Base Setup Information" $ do
+            runTest
+                ModeBase
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431"
+                "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736"
+                "3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d"
+                "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8"
+                ""
+                ""
+                ""
+                "f938558b5d72f1a23810b4be2ab4f84331acc02fc97babc53a52ae8218a355a96d8770ac83d07bea87e13c512a"
+                "af2d7e9ac9ae7e270f46ba1f975be53c09f8d875bdc8535458c2494e8a6eab251c03d0c22a56b8ca42c2063b84"
+                "3853fe2b4035195a573ffc53856e77058e15d9ea064de3e59f4961d0095250ee"
+                "2e8f0b54673c7029649d4eb9d5e33bf1872cf76d623ff164ac185da9e88c21a5"
+                "e9e43065102c3836401bed8c3c3c75ae46be1639869391d62c61f1ec7af54931"
+
+        it "A.1.2. PSK Setup Information" $ do
+            runTest
+                ModePsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "0ad0950d9fb9588e59690b74f1237ecdf1d775cd60be2eca57af5a4b0471c91b"
+                "463426a9ffb42bb17dbe6044b9abd1d4e4d95f9041cef0e99d7824eef2b6f588"
+                "9fed7e8c17387560e92cc6462a68049657246a09bfa8ade7aefe589672016366"
+                "c5eb01eb457fe6c6f57577c5413b931550a162c71a03ac8d196babbd4e5ce0fd"
+                ""
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "e52c6fed7f758d0cf7145689f21bc1be6ec9ea097fef4e959440012f4feb73fb611b946199e681f4cfc34db8ea"
+                "49f3b19b28a9ea9f43e8c71204c00d4a490ee7f61387b6719db765e948123b45b61633ef059ba22cd62437c8ba"
+                "dff17af354c8b41673567db6259fd6029967b4e1aad13023c2ae5df8f4f43bf6"
+                "6a847261d8207fe596befb52928463881ab493da345b10e1dcc645e3b94e2d95"
+                "8aff52b45a1be3a734bc7a41e20b4e055ad4c4d22104b0c20285a7c4302401cd"
+
+        it "A.1.3. PSK Setup Information" $ do
+            runTest
+                ModeAuth
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "23fb952571a14a25e3d678140cd0e5eb47a0961bb18afcf85896e5453c312e76"
+                "ff4442ef24fbc3c1ff86375b0be1e77e88a0de1e79b30896d73411c5ff4c3518"
+                "1632d5c2f71c2b38d0a8fcc359355200caa8b1ffdf28618080466c909cb69b2e"
+                "fdea67cf831f1ca98d8e27b1f6abeb5b7745e9d35348b80fa407ff6958f9137e"
+                "dc4a146313cce60a278a5323d321f051c5707e9c45ba21a3479fecdf76fc69dd"
+                ""
+                ""
+                "5fd92cc9d46dbf8943e72a07e42f363ed5f721212cd90bcfd072bfd9f44e06b80fd17824947496e21b680c141b"
+                "d3736bb256c19bfa93d79e8f80b7971262cb7c887e35c26370cfed62254369a1b52e3d505b79dd699f002bc8ed"
+                "28c70088017d70c896a8420f04702c5a321d9cbf0279fba899b59e51bac72c85"
+                "25dfc004b0892be1888c3914977aa9c9bbaf2c7471708a49e1195af48a6f29ce"
+                "5a0131813abc9a522cad678eb6bafaabc43389934adb8097d23c5ff68059eb64"
+        it "A.1.4. PSK Setup Information" $ do
+            runTest
+                ModeAuthPsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "820818d3c23993492cc5623ab437a48a0a7ca3e9639c140fe1e33811eb844b7c"
+                "14de82a5897b613616a00c39b87429df35bc2b426bcfd73febcb45e903490768"
+                "1d11a3cd247ae48e901939659bd4d79b6b959e1f3e7d66663fbc9412dd4e0976"
+                "cb29a95649dc5656c2d054c1aa0d3df0493155e9d5da6d7e344ed8b6a64a9423"
+                "fc1c87d2f3832adb178b431fce2ac77c7ca2fd680f3406c77b5ecdf818b119f4"
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "a84c64df1e11d8fd11450039d4fe64ff0c8a99fca0bd72c2d4c3e0400bc14a40f27e45e141a24001697737533e"
+                "4d19303b848f424fc3c3beca249b2c6de0a34083b8e909b6aa4c3688505c05ffe0c8f57a0a4c5ab9da127435d9"
+                "08f7e20644bb9b8af54ad66d2067457c5f9fcb2a23d9f6cb4445c0797b330067"
+                "52e51ff7d436557ced5265ff8b94ce69cf7583f49cdb374e6aad801fc063b010"
+                "a30c20370c026bbea4dca51cb63761695132d342bae33a6a11527d3e7679436d"
diff --git a/test/A2Spec.hs b/test/A2Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/A2Spec.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module A2Spec where
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+import Data.ByteString ()
+import Test.Hspec
+
+import Test
+
+kem_id :: KEM_ID
+kem_id = DHKEM_X25519_HKDF_SHA256
+kdf_id :: KDF_ID
+kdf_id = HKDF_SHA256
+aead_id :: AEAD_ID
+aead_id = ChaCha20Poly1305
+
+spec :: Spec
+spec = do
+    describe "A.2.  DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305" $ do
+        it "A.2.1. Base Setup Information" $ do
+            runTest
+                ModeBase
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a"
+                "f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600"
+                "4310ee97d88cc1f088a5576c77ab0cf5c3ac797f3d95139c6c84b5429c59662a"
+                "8057991eef8f1f1af18f4a9491d16a1ce333f695d4db8e38da75975c4478e0fb"
+                ""
+                ""
+                ""
+                "1c5250d8034ec2b784ba2cfd69dbdb8af406cfe3ff938e131f0def8c8b60b4db21993c62ce81883d2dd1b51a28"
+                "6b53c051e4199c518de79594e1c4ab18b96f081549d45ce015be002090bb119e85285337cc95ba5f59992dc98c"
+                "4bbd6243b8bb54cec311fac9df81841b6fd61f56538a775e7c80a9f40160606e"
+                "8c1df14732580e5501b00f82b10a1647b40713191b7c1240ac80e2b68808ba69"
+                "5acb09211139c43b3090489a9da433e8a30ee7188ba8b0a9a1ccf0c229283e53"
+        it "A.2.2. PSK Setup Information" $ do
+            runTest
+                ModePsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "2261299c3f40a9afc133b969a97f05e95be2c514e54f3de26cbe5644ac735b04"
+                "0c35fdf49df7aa01cd330049332c40411ebba36e0c718ebc3edf5845795f6321"
+                "13640af826b722fc04feaa4de2f28fbd5ecc03623b317834e7ff4120dbe73062"
+                "77d114e0212be51cb1d76fa99dd41cfd4d0166b08caa09074430a6c59ef17879"
+                ""
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "4a177f9c0d6f15cfdf533fb65bf84aecdc6ab16b8b85b4cf65a370e07fc1d78d28fb073214525276f4a89608ff"
+                "5c3cabae2f0b3e124d8d864c116fd8f20f3f56fda988c3573b40b09997fd6c769e77c8eda6cda4f947f5b704a8"
+                "813c1bfc516c99076ae0f466671f0ba5ff244a41699f7b2417e4c59d46d39f40"
+                "2745cf3d5bb65c333658732954ee7af49eb895ce77f8022873a62a13c94cb4e1"
+                "ad40e3ae14f21c99bfdebc20ae14ab86f4ca2dc9a4799d200f43a25f99fa78ae"
+        it "A.2.3. PSK Setup Information" $ do
+            runTest
+                ModeAuth
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "f7674cc8cd7baa5872d1f33dbaffe3314239f6197ddf5ded1746760bfc847e0e"
+                "c94619e1af28971c8fa7957192b7e62a71ca2dcdde0a7cc4a8a9e741d600ab13"
+                "1a478716d63cb2e16786ee93004486dc151e988b34b475043d3e0175bdb01c44"
+                "3ca22a6d1cda1bb9480949ec5329d3bf0b080ca4c45879c95eddb55c70b80b82"
+                "2def0cb58ffcf83d1062dd085c8aceca7f4c0c3fd05912d847b61f3e54121f05"
+                ""
+                ""
+                "ab1a13c9d4f01a87ec3440dbd756e2677bd2ecf9df0ce7ed73869b98e00c09be111cb9fdf077347aeb88e61bdf"
+                "3265c7807ffff7fdace21659a2c6ccffee52a26d270c76468ed74202a65478bfaedfff9c2b7634e24f10b71016"
+                "070cffafd89b67b7f0eeb800235303a223e6ff9d1e774dce8eac585c8688c872"
+                "2852e728568d40ddb0edde284d36a4359c56558bb2fb8837cd3d92e46a3a14a8"
+                "1df39dc5dd60edcbf5f9ae804e15ada66e885b28ed7929116f768369a3f950ee"
+
+        it "A.2.4. PSK Setup Information" $ do
+            runTest
+                ModeAuthPsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "656a2e00dc9990fd189e6e473459392df556e9a2758754a09db3f51179a3fc02"
+                "5e6dd73e82b856339572b7245d3cbb073a7561c0bee52873490e305cbb710410"
+                "a5099431c35c491ec62ca91df1525d6349cb8aa170c51f9581f8627be6334851"
+                "7b36a42822e75bf3362dfabbe474b3016236408becb83b859a6909e22803cb0c"
+                "90761c5b0a7ef0985ed66687ad708b921d9803d51637c8d1cb72d03ed0f64418"
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "9aa52e29274fc6172e38a4461361d2342585d3aeec67fb3b721ecd63f059577c7fe886be0ede01456ebc67d597"
+                "59460bacdbe7a920ef2806a74937d5a691d6d5062d7daafcad7db7e4d8c649adffe575c1889c5c2e3a49af8e3e"
+                "c23ebd4e7a0ad06a5dddf779f65004ce9481069ce0f0e6dd51a04539ddcbd5cd"
+                "ed7ff5ca40a3d84561067ebc8e01702bc36cf1eb99d42a92004642b9dfaadd37"
+                "d3bae066aa8da27d527d85c040f7dd6ccb60221c902ee36a82f70bcd62a60ee4"
diff --git a/test/A3Spec.hs b/test/A3Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/A3Spec.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module A3Spec where
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+import Data.ByteString ()
+import Test.Hspec
+
+import Test
+
+kem_id :: KEM_ID
+kem_id = DHKEM_P256_HKDF_SHA256
+kdf_id :: KDF_ID
+kdf_id = HKDF_SHA256
+aead_id :: AEAD_ID
+aead_id = AES_128_GCM
+
+spec :: Spec
+spec = do
+    describe "A.3. DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, AES-128-GCM" $ do
+        it "A.3.1. Base Setup Information" $ do
+            runTest
+                ModeBase
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b325ac98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4"
+                "4995788ef4b9d6132b249ce59a77281493eb39af373d236a1fe415cb0c2d7beb"
+                "04fe8c19ce0905191ebc298a9245792531f26f0cece2460639e8bc39cb7f706a826a779b4cf969b8a0e539c7f62fb3d30ad6aa8f80e30f1d128aafd68a2ce72ea0"
+                "f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2"
+                ""
+                ""
+                ""
+                "5ad590bb8baa577f8619db35a36311226a896e7342a6d836d8b7bcd2f20b6c7f9076ac232e3ab2523f39513434"
+                "fa6f037b47fc21826b610172ca9637e82d6e5801eb31cbd3748271affd4ecb06646e0329cbdf3c3cd655b28e82"
+                "5e9bc3d236e1911d95e65b576a8a86d478fb827e8bdfe77b741b289890490d4d"
+                "6cff87658931bda83dc857e6353efe4987a201b849658d9b047aab4cf216e796"
+                "d8f1ea7942adbba7412c6d431c62d01371ea476b823eb697e1f6e6cae1dab85a"
+        it "A.3.2. PSK Setup Information" $ do
+            runTest
+                ModePsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04305d35563527bce037773d79a13deabed0e8e7cde61eecee403496959e89e4d0ca701726696d1485137ccb5341b3c1c7aaee90a4a02449725e744b1193b53b5f"
+                "57427244f6cc016cddf1c19c8973b4060aa13579b4c067fd5d93a5d74e32a90f"
+                "040d97419ae99f13007a93996648b2674e5260a8ebd2b822e84899cd52d87446ea394ca76223b76639eccdf00e1967db10ade37db4e7db476261fcc8df97c5ffd1"
+                "438d8bcef33b89e0e9ae5eb0957c353c25a94584b0dd59c991372a75b43cb661"
+                ""
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "90c4deb5b75318530194e4bb62f890b019b1397bbf9d0d6eb918890e1fb2be1ac2603193b60a49c2126b75d0eb"
+                "9e223384a3620f4a75b5a52f546b7262d8826dea18db5a365feb8b997180b22d72dc1287f7089a1073a7102c27"
+                "a115a59bf4dd8dc49332d6a0093af8efca1bcbfd3627d850173f5c4a55d0c185"
+                "4517eaede0669b16aac7c92d5762dd459c301fa10e02237cd5aeb9be969430c4"
+                "164e02144d44b607a7722e58b0f4156e67c0c2874d74cf71da6ca48a4cbdc5e0"
+        it "A.3.3. PSK Setup Information" $ do
+            runTest
+                ModeAuth
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "042224f3ea800f7ec55c03f29fc9865f6ee27004f818fcbdc6dc68932c1e52e15b79e264a98f2c535ef06745f3d308624414153b22c7332bc1e691cb4af4d53454"
+                "6b8de0873aed0c1b2d09b8c7ed54cbf24fdf1dfc7a47fa501f918810642d7b91"
+                "04423e363e1cd54ce7b7573110ac121399acbc9ed815fae03b72ffbd4c18b01836835c5a09513f28fc971b7266cfde2e96afe84bb0f266920e82c4f53b36e1a78d"
+                "d929ab4be2e59f6954d6bedd93e638f02d4046cef21115b00cdda2acb2a4440e"
+                "1120ac99fb1fccc1e8230502d245719d1b217fe20505c7648795139d177f0de9"
+                ""
+                ""
+                "82ffc8c44760db691a07c5627e5fc2c08e7a86979ee79b494a17cc3405446ac2bdb8f265db4a099ed3289ffe19"
+                "b0a705a54532c7b4f5907de51c13dffe1e08d55ee9ba59686114b05945494d96725b239468f1229e3966aa1250"
+                "837e49c3ff629250c8d80d3c3fb957725ed481e59e2feb57afd9fe9a8c7c4497"
+                "594213f9018d614b82007a7021c3135bda7b380da4acd9ab27165c508640dbda"
+                "14fe634f95ca0d86e15247cca7de7ba9b73c9b9deb6437e1c832daf7291b79d5"
+
+        it "A.3.4. PSK Setup Information" $ do
+            runTest
+                ModeAuthPsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "046a1de3fc26a3d43f4e4ba97dbe24f7e99181136129c48fbe872d4743e2b131357ed4f29a7b317dc22509c7b00991ae990bf65f8b236700c82ab7c11a84511401"
+                "36f771e411cf9cf72f0701ef2b991ce9743645b472e835fe234fb4d6eb2ff5a0"
+                "04d824d7e897897c172ac8a9e862e4bd820133b8d090a9b188b8233a64dfbc5f725aa0aa52c8462ab7c9188f1c4872f0c99087a867e8a773a13df48a627058e1b3"
+                "bdf4e2e587afdf0930644a0c45053889ebcadeca662d7c755a353d5b4e2a8394"
+                "b0ed8721db6185435898650f7a677affce925aba7975a582653c4cb13c72d240"
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "b9f36d58d9eb101629a3e5a7b63d2ee4af42b3644209ab37e0a272d44365407db8e655c72e4fa46f4ff81b9246"
+                "51788c4e5d56276771032749d015d3eea651af0c7bb8e3da669effffed299ea1f641df621af65579c10fc09736"
+                "595ce0eff405d4b3bb1d08308d70a4e77226ce11766e0a94c4fdb5d90025c978"
+                "110472ee0ae328f57ef7332a9886a1992d2c45b9b8d5abc9424ff68630f7d38d"
+                "18ee4d001a9d83a4c67e76f88dd747766576cac438723bad0700a910a4d717e6"
diff --git a/test/A4Spec.hs b/test/A4Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/A4Spec.hs
@@ -0,0 +1,97 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module A4Spec where
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+import Data.ByteString ()
+import Test.Hspec
+
+import Test
+
+kem_id :: KEM_ID
+kem_id = DHKEM_P256_HKDF_SHA256
+kdf_id :: KDF_ID
+kdf_id = HKDF_SHA512
+aead_id :: AEAD_ID
+aead_id = AES_128_GCM
+
+spec :: Spec
+spec = do
+    describe "A.4. DHKEM(P-256, HKDF-SHA256), HKDF-SHA512, AES-128-GCM" $ do
+        it "A.4.1. Base Setup Information" $ do
+            runTest
+                ModeBase
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "0493ed86735bdfb978cc055c98b45695ad7ce61ce748f4dd63c525a3b8d53a15565c6897888070070c1579db1f86aaa56deb8297e64db7e8924e72866f9a472580"
+                "2292bf14bb6e15b8c81a0f45b7a6e93e32d830e48cca702e0affcfb4d07e1b5c"
+                "04085aa5b665dc3826f9650ccbcc471be268c8ada866422f739e2d531d4a8818a9466bc6b449357096232919ec4fe9070ccbac4aac30f4a1a53efcf7af90610edd"
+                "3ac8530ad1b01885960fab38cf3cdc4f7aef121eaa239f222623614b4079fb38"
+                ""
+                ""
+                ""
+                "d3cf4984931484a080f74c1bb2a6782700dc1fef9abe8442e44a6f09044c88907200b332003543754eb51917ba"
+                "d14414555a47269dfead9fbf26abb303365e40709a4ed16eaefe1f2070f1ddeb1bdd94d9e41186f124e0acc62d"
+                "a32186b8946f61aeead1c093fe614945f85833b165b28c46bf271abf16b57208"
+                "84998b304a0ea2f11809398755f0abd5f9d2c141d1822def79dd15c194803c2a"
+                "93fb9411430b2cfa2cf0bed448c46922a5be9beff20e2e621df7e4655852edbc"
+        it "A.4.2. PSK Setup Information" $ do
+            runTest
+                ModePsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04a307934180ad5287f95525fe5bc6244285d7273c15e061f0f2efb211c35057f3079f6e0abae200992610b25f48b63aacfcb669106ddee8aa023feed301901371"
+                "a5901ff7d6931959c2755382ea40a4869b1dec3694ed3b009dda2d77dd488f18"
+                "043f5266fba0742db649e1043102b8a5afd114465156719cea90373229aabdd84d7f45dabfc1f55664b888a7e86d594853a6cccdc9b189b57839cbbe3b90b55873"
+                "bc6f0b5e22429e5ff47d5969003f3cae0f4fec50e23602e880038364f33b8522"
+                ""
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "57624b6e320d4aba0afd11f548780772932f502e2ba2a8068676b2a0d3b5129a45b9faa88de39e8306da41d4cc"
+                "159d6b4c24bacaf2f5049b7863536d8f3ffede76302dace42080820fa51925d4e1c72a64f87b14291a3057e00a"
+                "8158bea21a6700d37022bb7802866edca30ebf2078273757b656ef7fc2e428cf"
+                "6a348ba6e0e72bb3ef22479214a139ef8dac57be34509a61087a12565473da8d"
+                "2f6d4f7a18ec48de1ef4469f596aada4afdf6d79b037ed3c07e0118f8723bffc"
+        it "A.4.3. PSK Setup Information" $ do
+            runTest
+                ModeAuth
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04fec59fa9f76f5d0f6c1660bb179cb314ed97953c53a60ab38f8e6ace60fd59178084d0dd66e0f79172992d4ddb2e91172ce24949bcebfff158dcc417f2c6e9c6"
+                "93cddd5288e7ef4884c8fe321d075df01501b993ff49ffab8184116f39b3c655"
+                "04378bad519aab406e04d0e5608bcca809c02d6afd2272d4dd03e9357bd0eee8adf84c8deba3155c9cf9506d1d4c8bfefe3cf033a75716cc3cc07295100ec96276"
+                "1ea4484be482bf25fdb2ed39e6a02ed9156b3e57dfb18dff82e4a048de990236"
+                "02b266d66919f7b08f42ae0e7d97af4ca98b2dae3043bb7e0740ccadc1957579"
+                ""
+                ""
+                "2480179d880b5f458154b8bfe3c7e8732332de84aabf06fc440f6b31f169e154157fa9eb44f2fa4d7b38a9236e"
+                "10cd81e3a816d29942b602a92884348171a31cbd0f042c3057c65cd93c540943a5b05115bd520c09281061935b"
+                "f03fbc82f321a0ab4840e487cb75d07aafd8e6f68485e4f7ff72b2f55ff24ad6"
+                "1ce0cadec0a8f060f4b5070c8f8888dcdfefc2e35819df0cd559928a11ff0891"
+                "70c405c707102fd0041ea716090753be47d68d238b111d542846bd0d84ba907c"
+        it "A.4.4. PSK Setup Information" $ do
+            runTest
+                ModeAuthPsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04801740f4b1b35823f7fb2930eac2efc8c4893f34ba111c0bb976e3c7d5dc0aef5a7ef0bf4057949a140285f774f1efc53b3860936b92279a11b68395d898d138"
+                "778f2254ae5d661d5c7fca8c4a7495a25bd13f26258e459159f3899df0de76c1"
+                "04a4ca7af2fc2cce48edbf2f1700983e927743a4e85bb5035ad562043e25d9a111cbf6f7385fac55edc5c9d2ca6ed351a5643de95c36748e11dbec98730f4d43e9"
+                "00510a70fde67af487c093234fc4215c1cdec09579c4b30cc8e48cb530414d0e"
+                "d743b20821e6326f7a26684a4beed7088b35e392114480ca9f6c325079dcf10b"
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "840669634db51e28df54f189329c1b727fd303ae413f003020aff5e26276aaa910fc4296828cb9d862c2fd7d16"
+                "d4680a48158d9a75fd09355878d6e33997a36ee01d4a8f22032b22373b795a941b7b9c5205ff99e0ff284beef4"
+                "c8c917e137a616d3d4e4c9fcd9c50202f366cb0d37862376bc79f9b72e8a8db9"
+                "33a5d4df232777008a06d0684f23bb891cfaef702f653c8601b6ad4d08dddddf"
+                "bed80f2e54f1285895c4a3f3b3625e6206f78f1ed329a0cfb5864f7c139b3c6a"
diff --git a/test/A5Spec.hs b/test/A5Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/A5Spec.hs
@@ -0,0 +1,97 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module A5Spec where
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+import Data.ByteString ()
+import Test.Hspec
+
+import Test
+
+kem_id :: KEM_ID
+kem_id = DHKEM_P256_HKDF_SHA256
+kdf_id :: KDF_ID
+kdf_id = HKDF_SHA256
+aead_id :: AEAD_ID
+aead_id = ChaCha20Poly1305
+
+spec :: Spec
+spec = do
+    describe "A.5. DHKEM(P-256, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305" $ do
+        it "A.5.1. Base Setup Information" $ do
+            runTest
+                ModeBase
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04c07836a0206e04e31d8ae99bfd549380b072a1b1b82e563c935c095827824fc1559eac6fb9e3c70cd3193968994e7fe9781aa103f5b50e934b5b2f387e381291"
+                "7550253e1147aae48839c1f8af80d2770fb7a4c763afe7d0afa7e0f42a5b3689"
+                "04a697bffde9405c992883c5c439d6cc358170b51af72812333b015621dc0f40bad9bb726f68a5c013806a790ec716ab8669f84f6b694596c2987cf35baba2a006"
+                "a4d1c55836aa30f9b3fbb6ac98d338c877c2867dd3a77396d13f68d3ab150d3b"
+                ""
+                ""
+                ""
+                "6469c41c5c81d3aa85432531ecf6460ec945bde1eb428cb2fedf7a29f5a685b4ccb0d057f03ea2952a27bb458b"
+                "f1564199f7e0e110ec9c1bcdde332177fc35c1adf6e57f8d1df24022227ffa8716862dbda2b1dc546c9d114374"
+                "9b13c510416ac977b553bf1741018809c246a695f45eff6d3b0356dbefe1e660"
+                "6c8b7be3a20a5684edecb4253619d9051ce8583baf850e0cb53c402bdcaf8ebb"
+                "477a50d804c7c51941f69b8e32fe8288386ee1a84905fe4938d58972f24ac938"
+        it "A.5.2. PSK Setup Information" $ do
+            runTest
+                ModePsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04f336578b72ad7932fe867cc4d2d44a718a318037a0ec271163699cee653fa805c1fec955e562663e0c2061bb96a87d78892bff0cc0bad7906c2d998ebe1a7246"
+                "7d6e4e006cee68af9b3fdd583a0ee8962df9d59fab029997ee3f456cbc857904"
+                "041eb8f4f20ab72661af369ff3231a733672fa26f385ffb959fd1bae46bfda43ad55e2d573b880831381d9367417f554ce5b2134fbba5235b44db465feffc6189e"
+                "12ecde2c8bc2d5d7ed2219c71f27e3943d92b344174436af833337c557c300b3"
+                ""
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "21433eaff24d7706f3ed5b9b2e709b07230e2b11df1f2b1fe07b3c70d5948a53d6fa5c8bed194020bd9df0877b"
+                "c74a764b4892072ea8c2c56b9bcd46c7f1e9ca8cb0a263f8b40c2ba59ac9c857033f176019562218769d3e0452"
+                "530bbc2f68f078dccc89cc371b4f4ade372c9472bafe4601a8432cbb934f528d"
+                "6e25075ddcc528c90ef9218f800ca3dfe1b8ff4042de5033133adb8bd54c401d"
+                "6f6fbd0d1c7733f796461b3235a856cc34f676fe61ed509dfc18fa16efe6be78"
+        it "A.5.3. PSK Setup Information" $ do
+            runTest
+                ModeAuth
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "040d5176aedba55bc41709261e9195c5146bb62d783031280775f32e507d79b5cbc5748b6be6359760c73cfe10ca19521af704ca6d91ff32fc0739527b9385d415"
+                "085fd5d5e6ce6497c79df960cac93710006b76217d8bcfafbd2bb2c20ea03c42"
+                "0444f6ee41818d9fe0f8265bffd016b7e2dd3964d610d0f7514244a60dbb7a11ece876bb110a97a2ac6a9542d7344bf7d2bd59345e3e75e497f7416cf38d296233"
+                "3cb2c125b8c5a81d165a333048f5dcae29a2ab2072625adad66dbb0f48689af9"
+                "39b19402e742d48d319d24d68e494daa4492817342e593285944830320912519"
+                ""
+                ""
+                "25881f219935eec5ba70d7b421f13c35005734f3e4d959680270f55d71e2f5cb3bd2daced2770bf3d9d4916872"
+                "653f0036e52a376f5d2dd85b3204b55455b7835c231255ae098d09ed138719b97185129786338ab6543f753193"
+                "56c4d6c1d3a46c70fd8f4ecda5d27c70886e348efb51bd5edeaa39ff6ce34389"
+                "d2d3e48ed76832b6b3f28fa84be5f11f09533c0e3c71825a34fb0f1320891b51"
+                "eb0d312b6263995b4c7761e64b688c215ffd6043ff3bad2368c862784cbe6eff"
+        it "A.5.4. PSK Setup Information" $ do
+            runTest
+                ModeAuthPsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "043539917ee26f8ae0aa5f784a387981b13de33124a3cde88b94672030183110f331400115855808244ff0c5b6ca6104483ac95724481d41bdcd9f15b430ad16f6"
+                "11b7e4de2d919240616a31ab14944cced79bc2372108bb98f6792e3b645fe546"
+                "04d383fd920c42d018b9d57fd73a01f1eee480008923f67d35169478e55d2e8817068daf62a06b10e0aad4a9e429fa7f904481be96b79a9c231a33e956c20b81b6"
+                "c29fc577b7e74d525c0043f1c27540a1248e4f2c8d297298e99010a92e94865c"
+                "53541bd995f874a67f8bfd8038afa67fd68876801f42ff47d0dc2a4deea067ae"
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "9eadfa0f954835e7e920ffe56dec6b31a046271cf71fdda55db72926e1d8fae94cc6280fcfabd8db71eaa65c05"
+                "e357ad10d75240224d4095c9f6150a2ed2179c0f878e4f2db8ca95d365d174d059ff8c3eb38ea9a65cfc8eaeb8"
+                "c52b4592cd33dd38b2a3613108ddda28dcf7f03d30f2a09703f758bfa8029c9a"
+                "2f03bebc577e5729e148554991787222b5c2a02b77e9b1ac380541f710e5a318"
+                "e01dd49e8bfc3d9216abc1be832f0418adf8b47a7b5a330a7436c31e33d765d7"
diff --git a/test/A6Spec.hs b/test/A6Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/A6Spec.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module A6Spec where
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+import Data.ByteString ()
+import Test.Hspec
+
+import Test
+
+kem_id :: KEM_ID
+kem_id = DHKEM_P521_HKDF_SHA512
+kdf_id :: KDF_ID
+kdf_id = HKDF_SHA512
+aead_id :: AEAD_ID
+aead_id = AES_256_GCM
+
+spec :: Spec
+spec = do
+    describe "A.6 DHKEM(P-521, HKDF-SHA512), HKDF-SHA512, AES-256-GCM" $ do
+        it "A.6.1. Base Setup Information" $ do
+            runTest
+                ModeBase
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "040138b385ca16bb0d5fa0c0665fbbd7e69e3ee29f63991d3e9b5fa740aab8900aaeed46ed73a49055758425a0ce36507c54b29cc5b85a5cee6bae0cf1c21f2731ece2013dc3fb7c8d21654bb161b463962ca19e8c654ff24c94dd2898de12051f1ed0692237fb02b2f8d1dc1c73e9b366b529eb436e98a996ee522aef863dd5739d2f29b0"
+                "014784c692da35df6ecde98ee43ac425dbdd0969c0c72b42f2e708ab9d535415a8569bdacfcc0a114c85b8e3f26acf4d68115f8c91a66178cdbd03b7bcc5291e374b"
+                "0401b45498c1714e2dce167d3caf162e45e0642afc7ed435df7902ccae0e84ba0f7d373f646b7738bbbdca11ed91bdeae3cdcba3301f2457be452f271fa6837580e661012af49583a62e48d44bed350c7118c0d8dc861c238c72a2bda17f64704f464b57338e7f40b60959480c0e58e6559b190d81663ed816e523b6b6a418f66d2451ec64"
+                "01462680369ae375e4b3791070a7458ed527842f6a98a79ff5e0d4cbde83c27196a3916956655523a6a2556a7af62c5cadabe2ef9da3760bb21e005202f7b2462847"
+                ""
+                ""
+                ""
+                "170f8beddfe949b75ef9c387e201baf4132fa7374593dfafa90768788b7b2b200aafcc6d80ea4c795a7c5b841a"
+                "d9ee248e220ca24ac00bbbe7e221a832e4f7fa64c4fbab3945b6f3af0c5ecd5e16815b328be4954a05fd352256"
+                "05e2e5bd9f0c30832b80a279ff211cc65eceb0d97001524085d609ead60d0412"
+                "fca69744bb537f5b7a1596dbf34eaa8d84bf2e3ee7f1a155d41bd3624aa92b63"
+                "f389beaac6fcf6c0d9376e20f97e364f0609a88f1bc76d7328e9104df8477013"
+
+        it "A.6.2. PSK Setup Information" $ do
+            runTest
+                ModePsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "040085eff0835cc84351f32471d32aa453cdc1f6418eaaecf1c2824210eb1d48d0768b368110fab21407c324b8bb4bec63f042cfa4d0868d19b760eb4beba1bff793b30036d2c614d55730bd2a40c718f9466faf4d5f8170d22b6df98dfe0c067d02b349ae4a142e0c03418f0a1479ff78a3db07ae2c2e89e5840f712c174ba2118e90fdcb"
+                "012e5cfe0daf5fe2a1cd617f4c4bae7c86f1f527b3207f115e262a98cc65268ec88cb8645aec73b7aa0a472d0292502d1078e762646e0c093cf873243d12c39915f6"
+                "04006917e049a2be7e1482759fb067ddb94e9c4f7f5976f655088dec45246614ff924ed3b385fc2986c0ecc39d14f907bf837d7306aada59dd5889086125ecd038ead400603394b5d81f89ebfd556a898cc1d6a027e143d199d3db845cb91c5289fb26c5ff80832935b0e8dd08d37c6185a6f77683347e472d1edb6daa6bd7652fea628fae"
+                "011bafd9c7a52e3e71afbdab0d2f31b03d998a0dc875dd7555c63560e142bde264428de03379863b4ec6138f813fa009927dc5d15f62314c56d4e7ff2b485753eb72"
+                ""
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "de69e9d943a5d0b70be3359a19f317bd9aca4a2ebb4332a39bcdfc97d5fe62f3a77702f4822c3be531aa7843a1"
+                "77a16162831f90de350fea9152cfc685ecfa10acb4f7994f41aed43fa5431f2382d078ec88baec53943984553e"
+                "62691f0f971e34de38370bff24deb5a7d40ab628093d304be60946afcdb3a936"
+                "76083c6d1b6809da088584674327b39488eaf665f0731151128452e04ce81bff"
+                "0c7cfc0976e25ae7680cf909ae2de1859cd9b679610a14bec40d69b91785b2f6"
+        it "A.6.3. PSK Setup Information" $ do
+            runTest
+                ModeAuth
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04017de12ede7f72cb101dab36a111265c97b3654816dcd6183f809d4b3d111fe759497f8aefdc5dbb40d3e6d21db15bdc60f15f2a420761bcaeef73b891c2b117e9cf01e29320b799bbc86afdc5ea97d941ea1c5bd5ebeeac7a784b3bab524746f3e640ec26ee1bd91255f9330d974f845084637ee0e6fe9f505c5b87c86a4e1a6c3096dd"
+                "0185f03560de87bb2c543ef03607f3c33ac09980000de25eabe3b224312946330d2e65d192d3b4aa46ca92fc5ca50736b624402d95f6a80dc04d1f10ae9517137261"
+                "04007d419b8834e7513d0e7cc66424a136ec5e11395ab353da324e3586673ee73d53ab34f30a0b42a92d054d0db321b80f6217e655e304f72793767c4231785c4a4a6e008f31b93b7a4f2b8cd12e5fe5a0523dc71353c66cbdad51c86b9e0bdfcd9a45698f2dab1809ab1b0f88f54227232c858accc44d9a8d41775ac026341564a2d749f4"
+                "013ef326940998544a899e15e1726548ff43bbdb23a8587aa3bef9d1b857338d87287df5667037b519d6a14661e9503cfc95a154d93566d8c84e95ce93ad05293a0b"
+                "001018584599625ff9953b9305849850d5e34bd789d4b81101139662fbea8b6508ddb9d019b0d692e737f66beae3f1f783e744202aaf6fea01506c27287e359fe776"
+                ""
+                ""
+                "0116aeb3a1c405c61b1ce47600b7ecd11d89b9c08c408b7e2d1e00a4d64696d12e6881dc61688209a8207427f9"
+                "37ece0cf6741f443e9d73b9966dc0b228499bb21fbf313948327231e70a18380e080529c0267f399ba7c539cc6"
+                "8d78748d632f95b8ce0c67d70f4ad1757e61e872b5941e146986804b3990154b"
+                "80a4753230900ea785b6c80775092801fe91183746479f9b04c305e1db9d1f4d"
+                "620b176d737cf366bcc20d96adb54ec156978220879b67923689e6dca36210ed"
+
+        it "A.6.4. PSK Setup Information" $ do
+            runTest
+                ModeAuthPsk
+                kem_id
+                kdf_id
+                aead_id
+                "4f6465206f6e2061204772656369616e2055726e"
+                "04000a5096a6e6e002c83517b494bfc2e36bfb8632fae8068362852b70d0ff71e560b15aff96741ecffb63d8ac3090c3769679009ac59a99a1feb4713c5f090fc0dbed01ad73c45d29d369e36744e9ed37d12f80700c16d816485655169a5dd66e4ddf27f2acffe0f56f7f77ea2b473b4bf0518b975d9527009a3d14e5a4957e3e8a9074f8"
+                "003430af19716084efeced1241bb1a5625b6c826f11ef31649095eb27952619e36f62a79ea28001ac452fb20ddfbb66e62c6c0b1be03c0d28c97794a1fb638207a83"
+                "0401655b5d3b7cfafaba30851d25edc44c6dd17d99410efbed8591303b4dbeea8cb1045d5255f9a60384c3bbd4a3386ae6e6fab341dc1f8db0eed5f0ab1aaac6d7838e00dadf8a1c2c64b48f89c633721e88369e54104b31368f26e35d04a442b0b428510fb23caada686add16492f333b0f7ba74c391d779b788df2c38d7a7f4778009d91"
+                "0053c0bc8c1db4e9e5c3e3158bfdd7fc716aef12db13c8515adf821dd692ba3ca53041029128ee19c8556e345c4bcb840bb7fd789f97fe10f17f0e2c6c2528072843"
+                "003f64675fc8914ec9e2b3ecf13585b26dbaf3d5d805042ba487a5070b8c5ac1d39b17e2161771cc1b4d0a3ba6e866f4ea4808684b56af2a49b5e5111146d45d9326"
+                "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+                "456e6e796e20447572696e206172616e204d6f726961"
+                "942a2a92e0817cf032ce61abccf4f3a7c5d21b794ed943227e07b7df2d6dd92c9b8a9371949e65cca262448ab7"
+                "c0a83b5ec3d7933a090f681717290337b4fede5bfaa0a40ec29f93acad742888a1513c649104c391c78d1d7f29"
+                "a39502ef5ca116aa1317bd9583dd52f15b0502b71d900fc8a622d19623d0cb5d"
+                "749eda112c4cfdd6671d84595f12cd13198fc3ef93ed72369178f344fe6e09c3"
+                "f8b4e72cefbff4ca6c4eabb8c0383287082cfcbb953d900aed4959afd0017095"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/Test.hs b/test/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Test.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Test (runTest) where
+
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Base16 as B16
+import Test.Hspec
+
+import Crypto.HPKE
+import Crypto.HPKE.Internal
+
+runTest
+    :: Mode
+    -> KEM_ID
+    -> KDF_ID
+    -> AEAD_ID
+    -> Info
+    -> ByteString -- pkEm
+    -> ByteString -- skEm
+    -> ByteString -- pkRm
+    -> ByteString -- skRm
+    -> ByteString -- skSm
+    -> PSK
+    -> PSK_ID
+    -> CipherText
+    -> CipherText
+    -> Key
+    -> Key
+    -> Key
+    -> IO ()
+runTest mode kem_id kdf_id aead_id _info _pkEm _skEm _pkRm _skRm _skSm _psk _psk_id _ct0 _ct1 _sec0 _sec1 _sec2 = do
+    (enc, ctxS) <-
+        setupS
+            defaultHPKEMap
+            mode
+            kem_id
+            kdf_id
+            aead_id
+            (Just skEm) -- key provided
+            mskSm -- auth
+            pkRm
+            info
+            psk
+            psk_id
+    ctxR <-
+        setupR
+            defaultHPKEMap
+            mode
+            kem_id
+            kdf_id
+            aead_id
+            skRm
+            mskSm -- auth
+            pkEm
+            info
+            psk
+            psk_id
+    enc `shouldBe` pkEm
+    ct0' <- seal ctxS aad0 pt
+    ct0' `shouldBe` ct0
+    pt0 <- open ctxR aad0 ct0
+    pt0 `shouldBe` pt
+    ct1' <- seal ctxS aad1 pt
+    ct1' `shouldBe` ct1
+    pt1 <- open ctxR aad1 ct1
+    pt1 `shouldBe` pt
+
+    exportS ctxS exporter_context0 32 `shouldBe` sec0
+    exportR ctxR exporter_context0 32 `shouldBe` sec0
+    exportS ctxS exporter_context1 32 `shouldBe` sec1
+    exportS ctxS exporter_context2 32 `shouldBe` sec2
+  where
+    info = B16.decodeLenient _info
+    pkEm = EncodedPublicKey $ B16.decodeLenient _pkEm
+    skEm = EncodedSecretKey $ B16.decodeLenient _skEm
+    pkRm = EncodedPublicKey $ B16.decodeLenient _pkRm
+    skRm = EncodedSecretKey $ B16.decodeLenient _skRm
+    mskSm
+        | _skSm == "" = Nothing
+        | otherwise = Just $ EncodedSecretKey $ B16.decodeLenient _skSm
+    psk = B16.decodeLenient _psk
+    psk_id = B16.decodeLenient _psk_id
+    ct0 = B16.decodeLenient _ct0
+    ct1 = B16.decodeLenient _ct1
+    sec0 = B16.decodeLenient _sec0
+    sec1 = B16.decodeLenient _sec1
+    sec2 = B16.decodeLenient _sec2
+
+aad0 :: AAD
+aad0 = "\x43\x6f\x75\x6e\x74\x2d\x30"
+aad1 :: AAD
+aad1 = "\x43\x6f\x75\x6e\x74\x2d\x31"
+pt :: PlainText
+pt = "Beauty is truth, truth beauty"
+exporter_context0 :: Info
+exporter_context0 = ""
+exporter_context1 :: Info
+exporter_context1 = "\x00"
+exporter_context2 :: Info
+exporter_context2 = "\x54\x65\x73\x74\x43\x6f\x6e\x74\x65\x78\x74"
