diff --git a/botan-bindings/CHANGELOG.md b/botan-bindings/CHANGELOG.md index db5174b..eca9710 100644 --- a/botan-bindings/CHANGELOG.md +++ b/botan-bindings/CHANGELOG.md @@ -8,7 +8,8 @@ None ### New features -None +* Support eXtendable Output Functions (XOF). See PR + [#142](https://github.com/haskell-cryptography/botan/pull/142). ### Minor changes diff --git a/botan-bindings/botan-bindings.cabal b/botan-bindings/botan-bindings.cabal index ba5107a..044c5d2 100644 --- a/botan-bindings/botan-bindings.cabal +++ b/botan-bindings/botan-bindings.cabal @@ -116,6 +116,7 @@ library Botan.Bindings.Version Botan.Bindings.View Botan.Bindings.X509 + Botan.Bindings.XOF Botan.Bindings.ZFEC build-depends: base >=4.16 && <4.23 diff --git a/botan-bindings/src/Botan/Bindings/Hash.hs b/botan-bindings/src/Botan/Bindings/Hash.hs index 701fad1..e4f99c1 100644 --- a/botan-bindings/src/Botan/Bindings/Hash.hs +++ b/botan-bindings/src/Botan/Bindings/Hash.hs @@ -182,7 +182,7 @@ foreign import capi safe "botan/ffi.h botan_hash_block_size" botan_hash_block_size :: BotanHash -- ^ __hash__: hash object -> Ptr CSize -- ^ __block_size__: output buffer to hold the hash function block size - -> IO CInt -- ^ 0 on success, a negative value on failure + -> IO CInt -- ^ returns the hash function block size -- | Send more input to the hash function foreign import capi safe "botan/ffi.h botan_hash_update" diff --git a/botan-bindings/src/Botan/Bindings/XOF.hs b/botan-bindings/src/Botan/Bindings/XOF.hs new file mode 100644 index 0000000..8f108f9 --- /dev/null +++ b/botan-bindings/src/Botan/Bindings/XOF.hs @@ -0,0 +1,118 @@ +{-| +Module : Botan.Bindings.XOF +Description : eXtendable Output Functions (XOFs) +Copyright : (c) 2026-2027, Haskell Foundation +License : BSD-3-Clause +Maintainer : joris@well-typed.com, leo@apotheca.io +Stability : experimental +Portability : POSIX + +An eXtendable Output Function (XOF) transforms an arbitrary length input message into an indefinite stream of output bits. Typically, it is illegal to call update() after the first call to output(). +-} + +{-# LANGUAGE CApiFFI #-} +{-# LANGUAGE OverloadedStrings #-} + +module Botan.Bindings.XOF ( + BotanXOFStruct + , BotanXOF (..) + , botan_xof_init + , botan_xof_copy_state + , botan_xof_block_size + , botan_xof_name + , botan_xof_accepts_input + , botan_xof_clear + , botan_xof_update + , botan_xof_output + , botan_xof_destroy + , pattern BOTAN_XOF_ASCON_XOF128 + , pattern BOTAN_XOF_SHAKE_128_XOF + , pattern BOTAN_XOF_SHAKE_256_XOF + ) where + +import Botan.Bindings.ConstPtr +import Data.String +import Data.Word +import Foreign.C.Types +import Foreign.ForeignPtr +import Foreign.Ptr +import Foreign.Storable + +-- | Opaque XOF struct +data {-# CTYPE "botan/ffi.h" "struct botan_xof_struct" #-} BotanXOFStruct + +-- | Botan XOF object +newtype {-# CTYPE "botan/ffi.h" "botan_xof_t" #-} BotanXOF + = MkBotanXOF { ptr :: Ptr BotanXOFStruct } + deriving newtype (Eq, Ord, Storable) + +-- | Frees all resources of the eXtendable Output Function +foreign import capi safe "botan/ffi.h &botan_xof_destroy" + botan_xof_destroy + :: FinalizerPtr BotanXOFStruct + +-- | Initialize an eXtendable Output Function +foreign import capi safe "botan/ffi.h botan_xof_init" + botan_xof_init + :: Ptr BotanXOF -- ^ __xof__: XOF object + -> ConstPtr CChar -- ^ __xof_name__: name of the XOF, e.g., "SHAKE-128" + -> Word32 -- ^ __flags__: should be 0 in current API revision, all other uses are reserved and return BOTAN_FFI_ERROR_BAD_FLAG + -> IO CInt -- ^ 0 on success, a negative value on failure + +-- | Copy the state of an eXtendable Output Function +foreign import capi safe "botan/ffi.h botan_xof_copy_state" + botan_xof_copy_state + :: Ptr BotanXOF -- ^ __dest__: destination XOF object + -> BotanXOF -- ^ __source__: source XOF object + -> IO CInt -- ^ 0 on success, a negative value on failure + +-- | Writes the block size of the eXtendable Output Function to *block_size +foreign import capi safe "botan/ffi.h botan_xof_block_size" + botan_xof_block_size + :: BotanXOF -- ^ __xof__: XOF object + -> Ptr CSize -- ^ __block_size__: output buffer to hold the XOF's block size + -> IO CInt -- ^ returns the XOF block size + +-- | Get the name of this eXtendable Output Function +foreign import capi safe "botan/ffi.h botan_xof_name" + botan_xof_name + :: BotanXOF -- ^ __xof__: XOF object to read + -> Ptr CChar -- ^ __name__: output buffer + -> Ptr CSize -- ^ __name_len__: on input, the length of buffer, on success the number of bytes written + -> IO CInt -- ^ 0 on success, a negative value on buffer preparation failure + +-- | Get the input/output state of this eXtendable Output Function. Typically, XOFs don't accept input as soon as the first output bytes were requested. +foreign import capi safe "botan/ffi.h botan_xof_accepts_input" + botan_xof_accepts_input + :: BotanXOF -- ^ __xof__: XOF object + -> IO CInt -- ^ 1 iff the XOF is still accepting input bytes + +-- | Reinitializes the state of the eXtendable Output Function +foreign import capi safe "botan/ffi.h botan_xof_clear" + botan_xof_clear + :: BotanXOF -- ^ __xof__: XOF object + -> IO CInt -- ^ 0 on success, a negative value on failure + +-- | Send more input to the XOF +foreign import capi safe "botan/ffi.h botan_xof_update" + botan_xof_update + :: BotanXOF -- ^ __xof__: XOF object + -> ConstPtr Word8 -- ^ __in__: input buffer + -> CSize -- ^ __in_len__: number of bytes to read from the input buffer + -> IO CInt -- ^ 0 on success, a negative value on failure + +-- | Generate output bytes from the XOF +foreign import capi safe "botan/ffi.h botan_xof_output" + botan_xof_output + :: BotanXOF -- ^ __xof__: XOF object + -> Ptr Word8 -- ^ __out__: output buffer + -> CSize -- ^ __out_len__: number of bytes to write into the output buffer + -> IO CInt -- ^ 0 on success, a negative value on failure + +pattern BOTAN_XOF_ASCON_XOF128 + , BOTAN_XOF_SHAKE_128_XOF + , BOTAN_XOF_SHAKE_256_XOF + :: (Eq a, IsString a) => a +pattern BOTAN_XOF_ASCON_XOF128 = "Ascon-XOF128" +pattern BOTAN_XOF_SHAKE_128_XOF = "SHAKE-128" +pattern BOTAN_XOF_SHAKE_256_XOF = "SHAKE-256" diff --git a/botan-low/botan-low.cabal b/botan-low/botan-low.cabal index 9c83111..7456054 100644 --- a/botan-low/botan-low.cabal +++ b/botan-low/botan-low.cabal @@ -123,6 +123,7 @@ library Botan.Low.Version Botan.Low.View Botan.Low.X509 + Botan.Low.XOF Botan.Low.ZFEC other-modules: @@ -195,6 +196,7 @@ test-suite test Test.Botan.Low.Utility Test.Botan.Low.Version Test.Botan.Low.X509 + Test.Botan.Low.XOF Test.Botan.Low.ZFEC Test.Util.ByteString Test.Util.Hspec diff --git a/botan-low/src/Botan/Low/XOF.hs b/botan-low/src/Botan/Low/XOF.hs new file mode 100644 index 0000000..fdc2fef --- /dev/null +++ b/botan-low/src/Botan/Low/XOF.hs @@ -0,0 +1,127 @@ +{-| +Module : Botan.Low.XOF +Description : eXtendable Output Functions (XOFs) +Copyright : (c) 2026-2027, Haskell Foundation +License : BSD-3-Clause +Maintainer : joris@well-typed.com, leo@apotheca.io +Stability : experimental +Portability : POSIX + +An eXtendable Output Function (XOF) transforms an arbitrary length input message into an indefinite stream of output bits. Typically, it is illegal to call update() after the first call to output(). +-} + +module Botan.Low.XOF ( + + -- * Usage + + XOF(..) + , XOFName + , XOFDigest + , withXOF + , xofInit + , xofCopyState + , xofBlockSize + , xofName + , xofAcceptsInput + , xofClear + , xofUpdate + , xofOutput + , xofDestroy + + -- * eXtendable Output Function algorithms + + , pattern ASCON_XOF128 + , pattern SHAKE128 + , pattern SHAKE256 + + -- * Convenience + + , allXOFs + ) where + +import Botan.Bindings.XOF +import Botan.Low.Error.Internal +import Botan.Low.Internal.ByteString +import Botan.Low.Make +import Botan.Low.Remake +import Data.ByteString +import Foreign.C.Types +import Foreign.ForeignPtr +import Foreign.Ptr + +newtype XOF = MkXOF { foreignPtr :: ForeignPtr BotanXOFStruct } + +withXOF :: XOF -> (BotanXOF -> IO a) -> IO a +xofDestroy :: XOF -> IO () +createXOF :: (Ptr BotanXOF -> IO CInt) -> IO XOF +(withXOF, xofDestroy, createXOF) + = mkBindings + MkBotanXOF (.ptr) + MkXOF (.foreignPtr) + botan_xof_destroy + +type XOFName = ByteString + +pattern ASCON_XOF128 + , SHAKE128 + , SHAKE256 + :: XOFName + +pattern ASCON_XOF128 = BOTAN_XOF_ASCON_XOF128 +pattern SHAKE128 = BOTAN_XOF_SHAKE_256_XOF +pattern SHAKE256 = BOTAN_XOF_SHAKE_256_XOF + +type XOFDigest = ByteString + +allXOFs :: [XOFName] +allXOFs = + [ ASCON_XOF128 + , SHAKE128 + , SHAKE256 + ] + +xofInit + :: XOFName -- ^ __xof_name__: name of the XOF, e.g., "SHAKE-128" + -> IO XOF -- ^ __xof__: XOF object +xofInit = mkCreateObjectCString createXOF $ \ out name -> + botan_xof_init out name 0 + +xofCopyState + :: XOF -- ^ __source__: source XOF object + -> IO XOF -- ^ __dest__: destination XOF object +xofCopyState source = withXOF source $ \ sourcePtr -> do + createXOF $ \ dest -> botan_xof_copy_state dest sourcePtr + +xofBlockSize + :: XOF -- ^ __xof__: XOF object + -> IO Int -- ^ __block_size__: output buffer to hold the XOF's block size +xofBlockSize = mkGetSize withXOF botan_xof_block_size + +xofName + :: XOF -- ^ __xof__: XOF object to read + -> IO XOFDigest -- ^ __name__: output buffer +xofName = mkGetCString withXOF botan_xof_name + +xofAcceptsInput + :: XOF -- ^ __xof__: XOF object + -> IO Bool -- ^ __accepts_input__: true iff the XOF is still accepting input bytes +xofAcceptsInput = mkGetBoolCode withXOF botan_xof_accepts_input + +xofClear + :: XOF -- ^ __xof__: XOF object + -> IO () +xofClear = mkAction withXOF botan_xof_clear + +xofUpdate + :: XOF -- ^ __xof__: XOF object + -> ByteString -- ^ __in__: input buffer + -> IO () +xofUpdate = mkWithObjectSetterCBytesLen withXOF botan_xof_update + +xofOutput + :: XOF -- ^ __xof__: XOF object + -> Int -- ^ __out_len__: number of bytes to write into the output buffer + -> IO XOFDigest -- ^ __out__: output buffer +xofOutput xof sz = withXOF xof $ \ xofPtr -> do + allocBytes sz $ \ digestPtr -> do + throwBotanIfNegative_ $ botan_xof_output xofPtr digestPtr $ fromIntegral sz diff --git a/botan-low/test/Main.hs b/botan-low/test/Main.hs index 4a56ec8..a35d660 100644 --- a/botan-low/test/Main.hs +++ b/botan-low/test/Main.hs @@ -35,6 +35,7 @@ import qualified Test.Botan.Low.TOTP import qualified Test.Botan.Low.Utility import qualified Test.Botan.Low.Version import qualified Test.Botan.Low.X509 +import qualified Test.Botan.Low.XOF import qualified Test.Botan.Low.ZFEC import Test.Tasty import Test.Tasty.HUnit @@ -76,6 +77,7 @@ tests = do totpTests <- Test.Botan.Low.TOTP.tests utilityTests <- Test.Botan.Low.Utility.tests x509Tests <- Test.Botan.Low.X509.tests + xofTests <- Test.Botan.Low.XOF.tests zfecTests <- Test.Botan.Low.ZFEC.tests pure $ testGroup "botan-low" [ bcryptTests @@ -117,5 +119,6 @@ tests = do , utilityTests , Test.Botan.Low.Version.tests , x509Tests + , xofTests , zfecTests ] diff --git a/botan-low/test/Test/Botan/Low/Hash.hs b/botan-low/test/Test/Botan/Low/Hash.hs index a7ee062..5249d0f 100644 --- a/botan-low/test/Test/Botan/Low/Hash.hs +++ b/botan-low/test/Test/Botan/Low/Hash.hs @@ -36,13 +36,28 @@ spec_hash = testSuite allHashes chars $ \ h -> do pass it "can copy the internal state" $ do ctx <- hashInit h - -- TODO: Populate with state and actually prove - _ctx' <- hashCopyState ctx - pass + -- Populate with state + hashUpdate ctx message + -- Copy and further populate the state + ctx' <- hashCopyState ctx + hashUpdate ctx' message + d' <- hashFinal ctx' + -- Further populate the original state equally + hashUpdate ctx message + d <- hashFinal ctx + -- Check if both states match by comparing their digests + d' `shouldBe` d it "can clear all internal state" $ do ctx <- hashInit h - -- TODO: Populate with state and actually prove + -- Populate with state + hashUpdate ctx message + d <- hashFinal ctx + -- Clear and repopulate with state hashClear ctx + hashUpdate ctx message + d' <- hashFinal ctx + -- Check if states match after clearing + d' `shouldBe` d pass it "can update the internal state with a single message block" $ do ctx <- hashInit h diff --git a/botan-low/test/Test/Botan/Low/XOF.hs b/botan-low/test/Test/Botan/Low/XOF.hs new file mode 100644 index 0000000..132a543 --- /dev/null +++ b/botan-low/test/Test/Botan/Low/XOF.hs @@ -0,0 +1,98 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Test.Botan.Low.XOF (tests) where + +import Botan.Low.XOF +import Control.Monad +import Data.ByteString +import Test.Hspec +import Test.Tasty +import Test.Tasty.Hspec +import Test.Util.ByteString +import Test.Util.Hspec + +tests :: IO TestTree +tests = do + specs <- testSpec "spec_xof" spec_xof + pure $ testGroup "Test.Botan.Low.XOF" [ + specs + ] + +message :: ByteString +message = "Fee fi fo fum! I smell the blood of an Englishman!" + +spec_xof :: Spec +spec_xof = testSuite allXOFs chars $ \ h -> do + it "can initialize a XOF context" $ do + _ctx <- xofInit h + pass + it "can copy the internal state" $ do + ctx <- xofInit h + -- Populate with state + xofUpdate ctx message + -- Copy and further populate the state + ctx' <- xofCopyState ctx + xofUpdate ctx' message + d' <- xofOutput ctx' 4 + -- Further populate the original state equally + xofUpdate ctx message + d <- xofOutput ctx 4 + -- Check if both states match by comparing their digests + d' `shouldBe` d + it "has a name" $ do + ctx <- xofInit h + _name <- xofName ctx + pass + it "can clear all internal state" $ do + ctx <- xofInit h + -- Populate with state + xofUpdate ctx message + d <- xofOutput ctx 4 + -- Clear and repopulate with state + xofClear ctx + xofUpdate ctx message + d' <- xofOutput ctx 4 + -- Check if states match after clearing + d' `shouldBe` d + pass + it "can update the internal state with a single message block" $ do + ctx <- xofInit h + xofUpdate ctx message + pass + it "can update the internal state with multiple message blocks" $ do + ctx <- xofInit h + forM_ (splitBlocks 4 message) $ xofUpdate ctx + pass + it "can output a single digest block" $ do + ctx <- xofInit h + xofUpdate ctx message + _d <- xofOutput ctx 4 + pass + it "can output multiple digest blocks" $ do + ctx <- xofInit h + -- Populate with state + xofUpdate ctx message + -- Single digest block for reference + ctx' <- xofCopyState ctx + d' <- xofOutput ctx' 5 + -- Multiple digest blocks of the same accumulated length + d1 <- xofOutput ctx 4 + d2 <- xofOutput ctx 1 + -- Check if XOF property for multiple digest blocks holds + d1 <> d2 `shouldBe` d' + pass + it "accepts input after initialization" $ do + ctx <- xofInit h + b <- xofAcceptsInput ctx + b `shouldBe` True + it "accepts input after updating the internal state" $ do + ctx <- xofInit h + xofUpdate ctx message + b <- xofAcceptsInput ctx + b `shouldBe` True + it "denies input after outputting a digest block" $ do + ctx <- xofInit h + xofUpdate ctx message + _d <- xofOutput ctx 4 + b <- xofAcceptsInput ctx + b `shouldBe` False