packages feed

hpke 0.3.0 → 0.4.0

raw patch · 13 files changed

+93/−73 lines, 13 filesdep −QuickCheckdep ~basedep ~base16-bytestringdep ~cryptonPVP ok

version bump matches the API change (PVP)

Dependencies removed: QuickCheck

Dependency ranges changed: base, base16-bytestring, crypton, ram

API changes (from Hackage documentation)

- Crypto.HPKE: nEnc :: KEM_ID -> Int
+ Crypto.HPKE: nEnc :: KEM_ID -> Maybe Int
- Crypto.HPKE: nTag :: AEAD_ID -> Int
+ Crypto.HPKE: nTag :: AEAD_ID -> Maybe Int

Files

ChangeLog.md view
@@ -1,5 +1,19 @@ # ChangeLog for hpke +## 0.4.0++* **Breaking**: `nEnc` and `nTag` return `Maybe Int`, with `Nothing` for an+  identifier they do not know, rather than `0`.+* `seal` is safe to call from several threads: each call takes its own+  sequence number, so no two share a nonce.  `open` is serialized.+* `seal` and `open` throw `MessageLimitReachedError` before the sequence+  number would wrap, as RFC 9180 section 5.2 requires.+* AEAD tags are compared in constant time.+* A ChaCha20-Poly1305 decryption failure is an `OpenError`, not a+  `SealError`.+* `Show` of an unknown `KDF_ID` or `KEM_ID` uses the type's name.+* The lower bound on base moves to 4.9.+ ## 0.3.0  * A receiver authenticates a sender by its **public** key.  `setupBaseR`,
Crypto/HPKE.hs view
@@ -51,21 +51,23 @@ import Crypto.HPKE.Types  -- | Length of "enc", aka sender's public key.+--   'Nothing' for a KEM not registered in RFC 9180. {- 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+nEnc :: KEM_ID -> Maybe Int+nEnc DHKEM_P256_HKDF_SHA256   = Just  65+nEnc DHKEM_P384_HKDF_SHA384   = Just  97+nEnc DHKEM_P521_HKDF_SHA512   = Just 133+nEnc DHKEM_X25519_HKDF_SHA256 = Just  32+nEnc DHKEM_X448_HKDF_SHA512   = Just  56+nEnc _                        = Nothing {- FOURMOLU_ENABLE -}  -- | Length of AEAD tag.+--   'Nothing' for an AEAD not registered in RFC 9180. {- FOURMOLU_DISABLE -}-nTag :: AEAD_ID -> Int-nTag AES_128_GCM      = 16-nTag AES_256_GCM      = 16-nTag ChaCha20Poly1305 = 16-nTag _                =  0+nTag :: AEAD_ID -> Maybe Int+nTag AES_128_GCM      = Just 16+nTag AES_256_GCM      = Just 16+nTag ChaCha20Poly1305 = Just 16+nTag _                = Nothing {- FOURMOLU_ENABLE -}
Crypto/HPKE/AEAD.hs view
@@ -35,8 +35,9 @@  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'+    (plain, tag) <- dec key nonce aad cipher'+    -- AuthTag's Eq is constant-time+    if tag == AuthTag (convert tag')         then Right plain         else Left $ OpenError "tag mismatch"   where@@ -105,7 +106,7 @@  decryptAes128gcm :: AeadDecrypt decryptAes128gcm key nonce aad cipher = case initAES key nonce of-    Nothing -> Left $ OpenError "decrypttAes128gcm"+    Nothing -> Left $ OpenError "decryptAes128gcm"     Just st -> Right $ simpleDecrypt (st :: AEAD AES128) aad cipher aes128tagLength  aes128tagLength :: Int@@ -170,7 +171,7 @@ decryptChacha20poly1305 key nonce aad cipher =     case CCP.aeadChacha20poly1305Init key nonce of         CryptoPassed st -> Right $ simpleDecrypt st aad cipher chacha20poly1305tagLength-        CryptoFailed _ -> Left $ SealError "decryptChacha20poly1305"+        CryptoFailed _ -> Left $ OpenError "decryptChacha20poly1305"  chacha20poly1305tagLength :: Int chacha20poly1305tagLength = 16
Crypto/HPKE/Context.hs view
@@ -14,10 +14,11 @@     exportR, ) where +import Control.Concurrent.MVar (MVar, modifyMVar, newMVar) import qualified Control.Exception as E import Data.ByteArray (xor) import qualified Data.ByteString as BS-import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Data.IORef (IORef, atomicModifyIORef', newIORef)  import Crypto.HPKE.Types @@ -33,7 +34,7 @@  -- | Context for receivers. data ContextR = ContextR-    { seqRefR :: IORef Integer+    { seqVarR :: MVar Integer     , openR :: Open     , nonceBaseR :: Nonce     , expandR :: Info -> Int -> Either HPKEError Key@@ -43,36 +44,36 @@  -- | Encryption. --   This throws 'HPKEError'.+--   Safe to call from multiple threads: each call gets its own nonce. 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+    seqI <- atomicModifyIORef' seqRefS $ \i -> (i + 1, i)+    checkLimit nonceBaseS seqI+    either E.throwIO return $ sealS (computeNonce nonceBaseS seqI) aad pt  -- | Decryption. --   This throws 'HPKEError'.+--   The sequence number advances only when decryption succeeds. 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+open ContextR{..} aad ct = modifyMVar seqVarR $ \seqI -> do+    checkLimit nonceBaseR seqI+    case openR (computeNonce nonceBaseR seqI) aad ct of         Left err -> E.throwIO err-        Right pt -> do-            let seqI' = seqI + 1-            writeIORef seqRefR seqI'-            return pt+        Right pt -> return (seqI + 1, pt) +computeNonce :: Nonce -> Integer -> Nonce+computeNonce nonce_base seqI = seqBS `xor` nonce_base+  where+    seqBS = i2ospOf_ (BS.length nonce_base) seqI :: ByteString++-- RFC 9180 section 5.2: the sequence number must not wrap around.+checkLimit :: Nonce -> Integer -> IO ()+checkLimit nonce_base seqI+    | seqI >= 2 ^ (8 * BS.length nonce_base) - 1 =+        E.throwIO $ MessageLimitReachedError "sequence number overflow"+    | otherwise = return ()+ ----------------------------------------------------------------  -- | Exporting secret.@@ -118,10 +119,10 @@     -> (Info -> Int -> Either HPKEError Key)     -> IO ContextR newContextR key nonce_base open' expand = do-    seqref <- newIORef 0+    seqvar <- newMVar 0     return $         ContextR-            { seqRefR = seqref+            { seqVarR = seqvar             , openR = open' key             , nonceBaseR = nonce_base             , expandR = expand
Crypto/HPKE/ID.hs view
@@ -86,7 +86,7 @@     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+    show (KDF_ID n)  = "KDF_ID 0x" ++ printf "%04x" n {- FOURMOLU_ENABLE -}  data KDFHash = forall h. (HashAlgorithm h, KDF h) => KDFHash h@@ -122,7 +122,7 @@     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+    show (KEM_ID n)               = "KEM_ID 0x" ++ printf "%04x" n {- FOURMOLU_ENABLE -}  ----------------------------------------------------------------
Crypto/HPKE/KDF.hs view
@@ -86,5 +86,5 @@     siz = hashDigestSize h     -- the hash's own digest size, so the length is in range by construction     shared_secret =-        either (const "") id $+        either (error . show) id $             labeledExpand suite eae_prk "shared_secret" kem_context siz
Crypto/HPKE/KeyPair.hs view
@@ -1,6 +1,9 @@ {-# LANGUAGE RecordWildCards #-} -module Crypto.HPKE.KeyPair where+module Crypto.HPKE.KeyPair (+    genKeyPair,+    toPublicKey,+) where  import qualified Control.Exception as E import Crypto.KEM (generateKeyPair)
Crypto/HPKE/KeySchedule.hs view
@@ -46,7 +46,7 @@     -> PSK     -> PSK_ID     -> SharedSecret-    -> Either HPKEError (Key, Nonce, Int, PRK h)+    -> Either HPKEError (Key, Nonce, PRK h) keySchedule h suite nk nn mode info psk psk_id shared_secret = do     key <- labeledExpand suite secret "key" key_schedule_context nk     base_nonce <- labeledExpand suite secret "base_nonce" key_schedule_context nn@@ -54,7 +54,7 @@         labeledExpand suite secret "exp" key_schedule_context $ hashDigestSize h     case toPRK exporter_secret of         Nothing -> Left $ KeyScheduleError "cannot convert to PRK"-        Just prk -> Right (key, base_nonce, 0, prk)+        Just prk -> Right (key, base_nonce, prk)   where     psk_id_hash = labeledExtract suite "" "psk_id_hash" psk_id :: PRK h     info_hash = labeledExtract suite "" "info_hash" info :: PRK h@@ -63,5 +63,3 @@             :: ByteString      secret = labeledExtract suite (convert shared_secret) "secret" psk :: PRK h--
Crypto/HPKE/Map.hs view
@@ -1,4 +1,3 @@- -- | Which algorithms this package will run, and under which identifiers. module Crypto.HPKE.Map (     HPKEMap (..),
Crypto/HPKE/Setup.hs view
@@ -34,7 +34,7 @@     -- ^ 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.+    --   @mode_base@ is used if 'Nothing'. @mode_auth@ is used, otherwise.     -> EncodedPublicKey     -- ^ Peer's public key.     -> Info@@ -56,7 +56,7 @@     -- ^ My secret key     -> Maybe EncodedPublicKey     -- ^ The sender's public key, for authentication.-    --   'mode_base' is used if 'Nothing'. 'mode_auth' is used, otherwise.+    --   @mode_base@ is used if 'Nothing'. @mode_auth@ is used, otherwise.     -> EncodedPublicKey     -- ^ The encapsulated key, @enc@.     -> Info@@ -80,15 +80,15 @@     -- ^ 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.+    --   @mode_psk@ is used if 'Nothing'. @mode_auth_psk@ 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+setupPSKS kem_id kdf_id aead_id mskEm mskSm =+    setupS defaultHPKEMap mode kem_id kdf_id aead_id mskEm mskSm   where     mode = case mskSm of         Nothing -> ModePsk@@ -104,7 +104,7 @@     -- ^ My secret key     -> Maybe EncodedPublicKey     -- ^ The sender's public key, for authentication.-    --   'mode_psk' is used if 'Nothing'. 'mode_auth_psk' is used, otherwise.+    --   @mode_psk@ is used if 'Nothing'. @mode_auth_psk@ is used, otherwise.     -> EncodedPublicKey     -- ^ The encapsulated key, @enc@.     -> Info@@ -129,8 +129,7 @@     -> 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.+    -- ^ My secret key, for the authenticated modes.     -> EncodedPublicKey     -- ^ Peer's public key.     -> Info@@ -146,7 +145,7 @@             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+            throwOnError keys $ \(key, nonce, prk) -> do                 let expand' = labeledExpand suite' prk "sec"                 ctx <- newContextS key nonce seal' expand'                 return (enc, ctx)@@ -175,7 +174,7 @@             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+            throwOnError keys $ \(key, nonce, prk) -> do                 let expand' = labeledExpand suite' prk "sec"                 newContextR key nonce open' expand' 
Crypto/HPKE/Types.hs view
@@ -22,7 +22,6 @@     PSK_ID,     Encap,     Decap,-    ExporterSecret,     -- rexport     CryptoFailable (..),     SharedSecret (..),@@ -154,7 +153,3 @@ lookupE k table = case lookup k table of     Nothing -> Left $ Unsupported $ show k     Just v -> Right v--------------------------------------------------------------------type ExporterSecret = ByteString
hpke.cabal view
@@ -1,6 +1,6 @@ cabal-version:      >=1.10 name:               hpke-version:            0.3.0+version:            0.4.0 license:            BSD3 license-file:       LICENSE maintainer:         kazu@iij.ad.jp@@ -31,11 +31,11 @@     default-language: Haskell2010     ghc-options:      -Wall     build-depends:-        base >=4.7 && <5,-        base16-bytestring,+        base >=4.9 && <5,+        base16-bytestring >=1.0 && <1.1,         bytestring,         crypton >=2.1.8 && <2.3,-        ram+        ram >=0.19 && <0.23      default-extensions: Strict StrictData @@ -59,10 +59,9 @@     ghc-options:        -Wall -threaded -rtsopts     build-depends:         base >=4.9 && <5,-        QuickCheck,         bytestring,-        base16-bytestring,-        crypton,+        base16-bytestring >=1.0 && <1.1,+        crypton >=2.1.8 && <2.3,         hpke,         hspec,-        ram+        ram >=0.19 && <0.23
test/Test.hs view
@@ -2,7 +2,9 @@  module Test (runTest) where +import Data.Bits (xor) import Data.ByteString (ByteString)+import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import Test.Hspec @@ -62,6 +64,9 @@     pt0 `shouldBe` pt     ct1' <- seal ctxS aad1 pt     ct1' `shouldBe` ct1+    -- a forged tag is refused, and the sequence number does not advance+    open ctxR aad1 (BS.init ct1 <> BS.singleton (BS.last ct1 `xor` 1))+        `shouldThrow` isOpenError     pt1 <- open ctxR aad1 ct1     pt1 `shouldBe` pt @@ -98,6 +103,10 @@     sec0 = B16.decodeLenient _sec0     sec1 = B16.decodeLenient _sec1     sec2 = B16.decodeLenient _sec2++isOpenError :: HPKEError -> Bool+isOpenError (OpenError _) = True+isOpenError _ = False  aad0 :: AAD aad0 = "\x43\x6f\x75\x6e\x74\x2d\x30"