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 +14/−0
- Crypto/HPKE.hs +14/−12
- Crypto/HPKE/AEAD.hs +5/−4
- Crypto/HPKE/Context.hs +26/−25
- Crypto/HPKE/ID.hs +2/−2
- Crypto/HPKE/KDF.hs +1/−1
- Crypto/HPKE/KeyPair.hs +4/−1
- Crypto/HPKE/KeySchedule.hs +2/−4
- Crypto/HPKE/Map.hs +0/−1
- Crypto/HPKE/Setup.hs +9/−10
- Crypto/HPKE/Types.hs +0/−5
- hpke.cabal +7/−8
- test/Test.hs +9/−0
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"