Repository: haskell-cryptography/libsodium-bindings
Pull request #189: Define some cryptographic functions in `sel` as pure
Author: login: intricate
Created: 2025-05-18T21:02:58Z | Merged: 2025-05-19T05:32:51Z

Closes #187 

Defines the following functions as pure:

- `Sel.Hashing.hashByteString`
- `Sel.Hashing.SHA256.hashByteString`
- `Sel.Hashing.SHA512.hashByteString`
- `Sel.Hashing.Short.hashByteString`
- `Sel.Hashing.Password.hashByteStringWithParams`
- `Sel.HMAC.SHA256.authenticate`
- `Sel.HMAC.SHA512.authenticate`
- `Sel.HMAC.SHA512_256.authenticate`
- `Sel.SecretKey.Authentication.authenticate`

Linked issue #187: Pure cryptographic functions in `sel`
I wanted to get some opinions on this. 

Might it not be safe to define a few of the cryptographic functions in `sel` as pure (using `unsafeDupablePerformIO`)?

Some examples:

- `Sel.Hashing.hashByteString`
- `Sel.Hashing.SHA256.hashByteString`
- `Sel.Hashing.SHA512.hashByteString`
- `Sel.Hashing.Short.hashByteString`
- `Sel.Hashing.Password.hashByteStringWithParams`
- `Sel.HMAC.SHA256.authenticate`
- `Sel.HMAC.SHA512.authenticate`
- `Sel.HMAC.SHA512_256.authenticate`
- `Sel.SecretKey.Authentication.authenticate`
- `Sel.PublicKey.Signature.signMessage`
Linked issue comments: user: login: Kleidukos
created_at: 2025-04-08T11:07:04Z
body: @kozross Do you see any potential risk with doing so?
author_association: MEMBER
user: login: Kleidukos
created_at: 2025-04-09T15:10:44Z
body: @intricate if their libsodium implementations are not secretly stateful, then you have my go-ahead.
author_association: MEMBER
user: login: intricate
created_at: 2025-04-09T15:32:51Z
body: I'll check them out, thanks.
author_association: CONTRIBUTOR
user: login: Kleidukos
created_at: 2025-05-15T06:29:24Z
body: @intricate Do you still want to proceed? I'd like to release a new major version after this change of API. :) If you cannot, I'd be happy to take on that work.
author_association: MEMBER
user: login: intricate
created_at: 2025-05-15T12:34:39Z
body: Hey @Kleidukos. I can likely get it done this weekend
author_association: CONTRIBUTOR
user: login: Kleidukos
created_at: 2025-05-15T12:38:43Z
body: @intricate fantastic, don't hesitate to ask if you have any question or face any trouble. :)
author_association: MEMBER
user: login: turion
created_at: 2025-10-10T13:18:37Z
body: Can you document in the haddocks why some functions are "pure" and others aren't? 
author_association: NONE
user: login: intricate
created_at: 2025-10-10T18:17:17Z
body: @turion: were there any particular examples that you were looking at? 

In `Sel.PublicKey.Signature.signMessage`, for example, I left some haddock there explaining why that function is impure. I'd be happy to PR docs for any other functions where you think the reason for impurity is unclear.
author_association: CONTRIBUTOR
user: login: turion
created_at: 2025-10-12T16:16:46Z
body: See for example [`decrypt`](https://hackage-content.haskell.org/package/sel-0.1.0.0/docs/Sel-PublicKey-Cipher.html#v:decrypt) which uses `unsafeDupablePerformIO`, while `encrypt` doesn't. I don't understand why one of them is treated that way and not the other.
author_association: NONE
user: login: intricate
created_at: 2025-10-13T02:05:40Z
body: I see what you mean about it not being very clear.

`encrypt` is impure because it also generates a random nonce for the `crypto_box` operation (I'm guessing this is done in an effort to prevent nonce misuse). So perhaps that should be documented.

It could also be nice to offer a pure variant of `encrypt` which allows the caller to specify a nonce (documented with a warning about misuse).
author_association: CONTRIBUTOR
user: login: Kleidukos
created_at: 2025-11-30T13:22:39Z
body: @intricate Do you want to take care of this? :) 
author_association: MEMBER
user: login: intricate
created_at: 2025-12-02T12:17:12Z
body: @Kleidukos: Sure. I might not be able to until a couple days from now, though.
author_association: CONTRIBUTOR
user: login: Kleidukos
created_at: 2025-12-02T12:18:01Z
body: @intricate absolutely fine. :)
author_association: MEMBER

Diff:
diff --git a/sel/src/Sel/HMAC/SHA256.hs b/sel/src/Sel/HMAC/SHA256.hs
index 55f700d..0b85c0b 100644
--- a/sel/src/Sel/HMAC/SHA256.hs
+++ b/sel/src/Sel/HMAC/SHA256.hs
@@ -117,9 +117,9 @@ authenticate
   -- ^ Message to authenticate
   -> AuthenticationKey
   -- ^ Secret key for authentication
-  -> IO AuthenticationTag
+  -> AuthenticationTag
   -- ^ Cryptographic tag for authentication
-authenticate message (AuthenticationKey authenticationKeyForeignPtr) =
+authenticate message (AuthenticationKey authenticationKeyForeignPtr) = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen message $ \(cString, cStringLen) -> do
     authenticationTagForeignPtr <-
       Foreign.mallocForeignPtrBytes
diff --git a/sel/src/Sel/HMAC/SHA512.hs b/sel/src/Sel/HMAC/SHA512.hs
index 3616825..84798b6 100644
--- a/sel/src/Sel/HMAC/SHA512.hs
+++ b/sel/src/Sel/HMAC/SHA512.hs
@@ -117,9 +117,9 @@ authenticate
   -- ^ Message to authenticate
   -> AuthenticationKey
   -- ^ Secret key for authentication
-  -> IO AuthenticationTag
+  -> AuthenticationTag
   -- ^ Cryptographic tag for authentication
-authenticate message (AuthenticationKey authenticationKeyForeignPtr) =
+authenticate message (AuthenticationKey authenticationKeyForeignPtr) = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen message $ \(cString, cStringLen) -> do
     authenticationTagForeignPtr <-
       Foreign.mallocForeignPtrBytes
diff --git a/sel/src/Sel/HMAC/SHA512_256.hs b/sel/src/Sel/HMAC/SHA512_256.hs
index fcc482c..74a170e 100644
--- a/sel/src/Sel/HMAC/SHA512_256.hs
+++ b/sel/src/Sel/HMAC/SHA512_256.hs
@@ -116,9 +116,9 @@ authenticate
   -- ^ Message to authenticate
   -> AuthenticationKey
   -- ^ Secret key for authentication
-  -> IO AuthenticationTag
+  -> AuthenticationTag
   -- ^ Cryptographic tag for authentication
-authenticate message (AuthenticationKey authenticationKeyForeignPtr) =
+authenticate message (AuthenticationKey authenticationKeyForeignPtr) = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen message $ \(cString, cStringLen) -> do
     authenticationTagForeignPtr <-
       Foreign.mallocForeignPtrBytes
diff --git a/sel/src/Sel/Hashing.hs b/sel/src/Sel/Hashing.hs
index de7b1f0..3a642d2 100644
--- a/sel/src/Sel/Hashing.hs
+++ b/sel/src/Sel/Hashing.hs
@@ -63,6 +63,7 @@ import LibSodium.Bindings.GenericHashing
   , cryptoGenericHashStateBytes
   , cryptoGenericHashUpdate
   )
+import System.IO.Unsafe (unsafeDupablePerformIO)
 
 import Sel.Internal
 import Sel.Internal.Sodium (binaryToHex)
@@ -174,8 +175,8 @@ instance Show Hash where
 -- Without a 'HashKey', hashing the same data twice will give the same result.
 --
 -- @since 0.0.1.0
-hashByteString :: Maybe HashKey -> StrictByteString -> IO Hash
-hashByteString mHashKey bytestring =
+hashByteString :: Maybe HashKey -> StrictByteString -> Hash
+hashByteString mHashKey bytestring = unsafeDupablePerformIO $
   case mHashKey of
     Just (HashKey fPtr) ->
       Foreign.withForeignPtr fPtr $ \keyPtr ->
diff --git a/sel/src/Sel/Hashing/Password.hs b/sel/src/Sel/Hashing/Password.hs
index f2799fe..6b03467 100644
--- a/sel/src/Sel/Hashing/Password.hs
+++ b/sel/src/Sel/Hashing/Password.hs
@@ -152,8 +152,8 @@ hashText text = hashByteString (Text.encodeUtf8 text)
 -- The hash is __not__ encoded in human-readable format.
 --
 -- @since 0.0.1.0
-hashByteStringWithParams :: Argon2Params -> Salt -> StrictByteString -> IO PasswordHash
-hashByteStringWithParams Argon2Params{opsLimit, memLimit} (Salt argonSalt) bytestring =
+hashByteStringWithParams :: Argon2Params -> Salt -> StrictByteString -> PasswordHash
+hashByteStringWithParams Argon2Params{opsLimit, memLimit} (Salt argonSalt) bytestring = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen bytestring $ \(cString, cStringLen) -> do
     BS.unsafeUseAsCStringLen argonSalt $ \(saltString, _) -> do
       hashForeignPtr <- mallocForeignPtrBytes (fromIntegral cryptoPWHashStrBytes)
diff --git a/sel/src/Sel/Hashing/SHA256.hs b/sel/src/Sel/Hashing/SHA256.hs
index e85a11d..b3c20d1 100644
--- a/sel/src/Sel/Hashing/SHA256.hs
+++ b/sel/src/Sel/Hashing/SHA256.hs
@@ -56,6 +56,7 @@ import LibSodium.Bindings.SHA2
   , cryptoHashSHA256StateBytes
   , cryptoHashSHA256Update
   )
+import System.IO.Unsafe (unsafeDupablePerformIO)
 
 import Sel.Internal
 import Sel.Internal.Sodium (binaryToHex)
@@ -130,8 +131,8 @@ instance Show Hash where
 -- | Hash a 'StrictByteString' with the SHA-256 algorithm.
 --
 -- @since 0.0.1.0
-hashByteString :: StrictByteString -> IO Hash
-hashByteString bytestring =
+hashByteString :: StrictByteString -> Hash
+hashByteString bytestring = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen bytestring $ \(cString, cStringLen) -> do
     hashForeignPtr <- Foreign.mallocForeignPtrBytes (fromIntegral cryptoHashSHA256Bytes)
     Foreign.withForeignPtr hashForeignPtr $ \hashPtr ->
@@ -145,7 +146,7 @@ hashByteString bytestring =
 -- | Hash a UTF8-encoded strict 'Text' with the SHA-256 algorithm.
 --
 -- @since 0.0.1.0
-hashText :: Text -> IO Hash
+hashText :: Text -> Hash
 hashText text = hashByteString (Text.encodeUtf8 text)
 
 -- == Displaying
diff --git a/sel/src/Sel/Hashing/SHA512.hs b/sel/src/Sel/Hashing/SHA512.hs
index 348b026..cb599a3 100644
--- a/sel/src/Sel/Hashing/SHA512.hs
+++ b/sel/src/Sel/Hashing/SHA512.hs
@@ -56,6 +56,7 @@ import LibSodium.Bindings.SHA2
   , cryptoHashSHA512StateBytes
   , cryptoHashSHA512Update
   )
+import System.IO.Unsafe (unsafeDupablePerformIO)
 
 import Sel.Internal
 import Sel.Internal.Sodium (binaryToHex)
@@ -155,8 +156,8 @@ hashToBinary (Hash fPtr) =
 -- | Hash a 'StrictByteString' with the SHA-512 algorithm.
 --
 -- @since 0.0.1.0
-hashByteString :: StrictByteString -> IO Hash
-hashByteString bytestring =
+hashByteString :: StrictByteString -> Hash
+hashByteString bytestring = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen bytestring $ \(cString, cStringLen) -> do
     hashForeignPtr <- Foreign.mallocForeignPtrBytes (fromIntegral cryptoHashSHA512Bytes)
     Foreign.withForeignPtr hashForeignPtr $ \hashPtr ->
@@ -170,7 +171,7 @@ hashByteString bytestring =
 -- | Hash a UTF8-encoded strict 'Text' with the SHA-512 algorithm.
 --
 -- @since 0.0.1.0
-hashText :: Text -> IO Hash
+hashText :: Text -> Hash
 hashText text = hashByteString (Text.encodeUtf8 text)
 
 -- ** Hashing a multi-parts message
diff --git a/sel/src/Sel/Hashing/Short.hs b/sel/src/Sel/Hashing/Short.hs
index e899d6c..2fe178e 100644
--- a/sel/src/Sel/Hashing/Short.hs
+++ b/sel/src/Sel/Hashing/Short.hs
@@ -122,8 +122,8 @@ hashByteString
   -- ^ Random key produced by 'newKey'
   -> StrictByteString
   -- ^ Data to hash
-  -> IO ShortHash
-hashByteString (ShortHashKey keyFPtr) message =
+  -> ShortHash
+hashByteString (ShortHashKey keyFPtr) message = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen message $ \(cString, cStringLen) -> do
     shortHashFPtr <- Foreign.mallocForeignPtrBytes (fromIntegral cryptoShortHashSipHashX24Bytes)
     Foreign.withForeignPtr keyFPtr $ \keyPtr ->
@@ -149,7 +149,7 @@ hashText
   -- ^ Random key produced by 'newKey'
   -> Text
   -- ^ UTF-8 encoded data to hash
-  -> IO ShortHash
+  -> ShortHash
 hashText key message = hashByteString key (Text.encodeUtf8 message)
 
 -- | Convert a 'ShortHash' to a strict binary 'StrictByteString'.
diff --git a/sel/src/Sel/PublicKey/Signature.hs b/sel/src/Sel/PublicKey/Signature.hs
index 3a608af..e66bcbe 100644
--- a/sel/src/Sel/PublicKey/Signature.hs
+++ b/sel/src/Sel/PublicKey/Signature.hs
@@ -162,6 +162,15 @@ generateKeyPair = do
 
 -- | Sign a message.
 --
+-- Note that, if @libsodium@ is compiled with the @ED25519_NONDETERMINISTIC@
+-- macro defined, this function will produce non-deterministic (but also
+-- non-standard) Ed25519 signatures. If @libsodium@ hasn't been compiled with
+-- the @ED25519_NONDETERMINISTIC@ macro defined, it's safe to call this
+-- function in a pure context with 'unsafeDupablePerformIO'.
+--
+-- For more information, see the
+-- [@libsodium@ docs](https://doc.libsodium.org/public-key_cryptography/public-key_signatures#notes).
+--
 -- @since 0.0.1.0
 signMessage :: StrictByteString -> SecretKey -> IO SignedMessage
 signMessage message (SecretKey skFPtr) =
diff --git a/sel/src/Sel/SecretKey/Authentication.hs b/sel/src/Sel/SecretKey/Authentication.hs
index ab9259d..2321229 100644
--- a/sel/src/Sel/SecretKey/Authentication.hs
+++ b/sel/src/Sel/SecretKey/Authentication.hs
@@ -76,7 +76,7 @@ import Sel.Internal.Sodium (binaryToHex)
 -- >   authKey <- Auth.newAuthenticationKey
 -- >   -- An authentication tag is computed for the message by the server
 -- >   let message = "Hello, world!"
--- >   tag <- Auth.authenticate message
+-- >       tag = Auth.authenticate message
 -- >   -- The server sends the message and its authentication tag
 -- >   -- […]
 -- >   -- The recipient of the message uses the shared secret to validate the message's tag
@@ -91,9 +91,9 @@ authenticate
   -- ^ Message to authenticate
   -> AuthenticationKey
   -- ^ Secret key for authentication
-  -> IO AuthenticationTag
+  -> AuthenticationTag
   -- ^ Cryptographic tag for authentication
-authenticate message (AuthenticationKey authenticationKeyForeignPtr) =
+authenticate message (AuthenticationKey authenticationKeyForeignPtr) = unsafeDupablePerformIO $
   BS.unsafeUseAsCStringLen message $ \(cString, cStringLen) -> do
     authenticationTagForeignPtr <-
       Foreign.mallocForeignPtrBytes
diff --git a/sel/test/Test/HMAC.hs b/sel/test/Test/HMAC.hs
index c7dac2a..62aeda3 100644
--- a/sel/test/Test/HMAC.hs
+++ b/sel/test/Test/HMAC.hs
@@ -42,7 +42,7 @@ spec =
 testSingleHMACSHA256Hashing :: Assertion
 testSingleHMACSHA256Hashing = do
   key <- SHA256.newAuthenticationKey
-  tag <- SHA256.authenticate "Hello, world!" key
+  let tag = SHA256.authenticate "Hello, world!" key
   assertBool "message is verified" $
     SHA256.verify tag key "Hello, world!"
 
@@ -67,7 +67,7 @@ testHMAC256AuthenticationKeySerialisation = do
 testHMAC256AuthenticationTagSerialisation :: Assertion
 testHMAC256AuthenticationTagSerialisation = do
   key <- SHA256.newAuthenticationKey
-  tag1 <- SHA256.authenticate "Hello, world!" key
+  let tag1 = SHA256.authenticate "Hello, world!" key
   let hexTag = SHA256.authenticationTagToHexByteString tag1
   tag2 <- assertRight $ SHA256.authenticationTagFromHexByteString hexTag
   assertEqual "Roundtripping authentication key" tag1 tag2
@@ -77,7 +77,7 @@ testHMAC256AuthenticationTagSerialisation = do
 testSingleHMACSHA512Hashing :: Assertion
 testSingleHMACSHA512Hashing = do
   key <- SHA512.newAuthenticationKey
-  tag <- SHA512.authenticate "Hello, world!" key
+  let tag = SHA512.authenticate "Hello, world!" key
   assertBool "message is verified" $
     SHA512.verify tag key "Hello, world!"
 
@@ -102,7 +102,7 @@ testHMAC512AuthenticationKeySerialisation = do
 testHMAC512AuthenticationTagSerialisation :: Assertion
 testHMAC512AuthenticationTagSerialisation = do
   key <- SHA512.newAuthenticationKey
-  tag1 <- SHA512.authenticate "Hello, world!" key
+  let tag1 = SHA512.authenticate "Hello, world!" key
   let hexTag = SHA512.authenticationTagToHexByteString tag1
   tag2 <- assertRight $ SHA512.authenticationTagFromHexByteString hexTag
   assertEqual "Roundtripping authentication key" tag1 tag2
@@ -112,7 +112,7 @@ testHMAC512AuthenticationTagSerialisation = do
 testSingleHMACSHA512_256Hashing :: Assertion
 testSingleHMACSHA512_256Hashing = do
   key <- SHA512_256.newAuthenticationKey
-  tag <- SHA512_256.authenticate "Hello, world!" key
+  let tag = SHA512_256.authenticate "Hello, world!" key
   assertBool "message is verified" $
     SHA512_256.verify tag key "Hello, world!"
 
@@ -137,7 +137,7 @@ testHMAC512_256AuthenticationKeySerialisation = do
 testHMAC512_256AuthenticationTagSerialisation :: Assertion
 testHMAC512_256AuthenticationTagSerialisation = do
   key <- SHA512_256.newAuthenticationKey
-  tag1 <- SHA512_256.authenticate "Hello, world!" key
+  let tag1 = SHA512_256.authenticate "Hello, world!" key
   let hexTag = SHA512_256.authenticationTagToHexByteString tag1
   tag2 <- assertRight $ SHA512_256.authenticationTagFromHexByteString hexTag
   assertEqual "Roundtripping authentication key" tag1 tag2
diff --git a/sel/test/Test/Hashing.hs b/sel/test/Test/Hashing.hs
index 9b2afa4..7b101f0 100644
--- a/sel/test/Test/Hashing.hs
+++ b/sel/test/Test/Hashing.hs
@@ -24,7 +24,7 @@ spec =
 
 testCryptoGenericHashWithoutKey :: Assertion
 testCryptoGenericHashWithoutKey = do
-  expected <- Hashing.hashToHexByteString <$> Hashing.hashByteString Nothing "test test"
+  let expected = Hashing.hashToHexByteString $ Hashing.hashByteString Nothing "test test"
   assertEqual
     "Hashed test string is consistent without key"
     expected
@@ -54,7 +54,7 @@ testCryptoGenericHashWithKey =
 testMultipartHahsing :: Assertion
 testMultipartHahsing = do
   hashKey <- Hashing.newHashKey
-  expectedHash <- Hashing.hashByteString (Just hashKey) "test test"
+  let expectedHash = Hashing.hashByteString (Just hashKey) "test test"
   actualHash <- Hashing.withMultipart (Just hashKey) $ \multipartState -> do
     let message1 = "test "
     Hashing.updateMultipart multipartState message1
diff --git a/sel/test/Test/Hashing/Password.hs b/sel/test/Test/Hashing/Password.hs
index 812cc7f..1d8e6cc 100644
--- a/sel/test/Test/Hashing/Password.hs
+++ b/sel/test/Test/Hashing/Password.hs
@@ -46,8 +46,8 @@ testHashPasswordWSalt = do
       cmpPWHashes = on (==) Sel.passwordHashToByteString
 
   salt1 <- Sel.genSalt
-  hashOrig <- hashWSalt salt1 password
-  hashOrig' <- hashWSalt salt1 password
+  let hashOrig = hashWSalt salt1 password
+      hashOrig' = hashWSalt salt1 password
   assertBool
     "Password hashing with salt is consistent"
     (cmpPWHashes hashOrig hashOrig')
@@ -58,7 +58,7 @@ testHashPasswordWSalt = do
     (not $ cmpPWHashes hashOrig hashWoSalt)
 
   salt2 <- Sel.genSalt
-  hashWNewSalt <- hashWSalt salt2 password
+  let hashWNewSalt = hashWSalt salt2 password
   assertBool
     "Password hashing differs with a new salt"
     (not $ cmpPWHashes hashOrig hashWNewSalt)
diff --git a/sel/test/Test/Hashing/SHA2.hs b/sel/test/Test/Hashing/SHA2.hs
index 2e90689..a6c02fb 100644
--- a/sel/test/Test/Hashing/SHA2.hs
+++ b/sel/test/Test/Hashing/SHA2.hs
@@ -28,7 +28,7 @@ spec =
 testSingleHashSHA512 :: Assertion
 testSingleHashSHA512 = do
   let password = "hunter2" :: Text
-  actual <- SHA512.hashText password
+      actual = SHA512.hashText password
   assertEqual
     "SHA-512 hashing is consistent"
     (SHA512.hashToHexByteString actual)
@@ -47,7 +47,7 @@ testMultipartHashSH512 = do
 testSingleHashSHA256 :: Assertion
 testSingleHashSHA256 = do
   let password = "hunter2" :: Text
-  actual <- SHA256.hashText password
+      actual = SHA256.hashText password
   assertEqual
     "SHA-256 hashing is consistent"
     (SHA256.hashToHexByteString actual)
diff --git a/sel/test/Test/Hashing/Short.hs b/sel/test/Test/Hashing/Short.hs
index 6bdb2f6..e221b82 100644
--- a/sel/test/Test/Hashing/Short.hs
+++ b/sel/test/Test/Hashing/Short.hs
@@ -20,7 +20,7 @@ testHashPassword :: Assertion
 testHashPassword = do
   let key = fromJust $ Short.hexTextToShortHashKey "9301a3c5eedf2d783b72dc41fb907964"
   let input = "kwak kwak" :: Text
-  hash <- Short.hashText key input
+  let hash = Short.hashText key input
   assertEqual
     "input hashing is consistent"
     (Short.shortHashToHexText hash)
diff --git a/sel/test/Test/SecretKey/Authentication.hs b/sel/test/Test/SecretKey/Authentication.hs
index ad08256..8b73661 100644
--- a/sel/test/Test/SecretKey/Authentication.hs
+++ b/sel/test/Test/SecretKey/Authentication.hs
@@ -20,7 +20,7 @@ spec =
 testAuthenticateMessage :: Assertion
 testAuthenticateMessage = do
   key <- newAuthenticationKey
-  tag <- authenticate "hello, world" key
+  let tag = authenticate "hello, world" key
   assertBool
     "Tag verified"
     (verify tag key "hello, world")
@@ -38,8 +38,8 @@ testAuthKeySerdeRoundtrip = do
 testAuthTagSerdeRoundtrip :: Assertion
 testAuthTagSerdeRoundtrip = do
   key <- newAuthenticationKey
-  expectedTag <- authenticate "hello, world" key
-  let hexTag = authenticationTagToHexByteString expectedTag
+  let expectedTag = authenticate "hello, world" key
+      hexTag = authenticationTagToHexByteString expectedTag
   actualTag <- assertRight $ authenticationTagFromHexByteString hexTag
   assertEqual
     "Tag is expected"
diff --git a/sel/test/package-api-9.6.6.txt b/sel/test/package-api-9.6.6.txt
index 7a64bc5..fdfd84b 100644
--- a/sel/test/package-api-9.6.6.txt
+++ b/sel/test/package-api-9.6.6.txt
@@ -14,7 +14,7 @@ module Sel.HMAC.SHA256 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.HMAC.SHA256.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoAuthHMACSHA256State)
-  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToBinary :: AuthenticationTag -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
@@ -34,7 +34,7 @@ module Sel.HMAC.SHA512 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.HMAC.SHA512.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoAuthHMACSHA512State)
-  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToBinary :: AuthenticationTag -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
@@ -54,7 +54,7 @@ module Sel.HMAC.SHA512_256 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.HMAC.SHA512_256.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoAuthHMACSHA512256State)
-  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToBinary :: AuthenticationTag -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
@@ -74,7 +74,7 @@ module Sel.Hashing where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.Hashing.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.GenericHashing.CryptoGenericHashState)
-  hashByteString :: GHC.Maybe.Maybe HashKey -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO Hash
+  hashByteString :: GHC.Maybe.Maybe HashKey -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Hash
   hashToBinary :: Hash -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
   hashToHexByteString :: Hash -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
   hashToHexText :: Hash -> Data.Text.Internal.Text
@@ -95,7 +95,7 @@ module Sel.Hashing.Password where
   defaultArgon2Params :: Argon2Params
   genSalt :: GHC.Types.IO Salt
   hashByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO PasswordHash
-  hashByteStringWithParams :: Argon2Params -> Salt -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO PasswordHash
+  hashByteStringWithParams :: Argon2Params -> Salt -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> PasswordHash
   hashText :: Data.Text.Internal.Text -> GHC.Types.IO PasswordHash
   hexByteStringToSalt :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Maybe.Maybe Salt
   hexTextToSalt :: Data.Text.Internal.Text -> GHC.Maybe.Maybe Salt
@@ -115,8 +115,8 @@ module Sel.Hashing.SHA256 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.Hashing.SHA256.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoHashSHA256State)
-  hashByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO Hash
-  hashText :: Data.Text.Internal.Text -> GHC.Types.IO Hash
+  hashByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Hash
+  hashText :: Data.Text.Internal.Text -> Hash
   hashToBinary :: Hash -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
   hashToHexByteString :: Hash -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
   hashToHexText :: Hash -> Data.Text.Internal.Text
@@ -129,8 +129,8 @@ module Sel.Hashing.SHA512 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.Hashing.SHA512.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoHashSHA512State)
-  hashByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO Hash
-  hashText :: Data.Text.Internal.Text -> GHC.Types.IO Hash
+  hashByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Hash
+  hashText :: Data.Text.Internal.Text -> Hash
   hashToBinary :: Hash -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
   hashToHexByteString :: Hash -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
   hashToHexText :: Hash -> Data.Text.Internal.Text
@@ -145,8 +145,8 @@ module Sel.Hashing.Short where
   type ShortHashingException :: *
   data ShortHashingException = ShortHashingException
   binaryToShortHashKey :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Maybe.Maybe ShortHashKey
-  hashByteString :: ShortHashKey -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO ShortHash
-  hashText :: ShortHashKey -> Data.Text.Internal.Text -> GHC.Types.IO ShortHash
+  hashByteString :: ShortHashKey -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> ShortHash
+  hashText :: ShortHashKey -> Data.Text.Internal.Text -> ShortHash
   hexByteStringToShortHashKey :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> GHC.Maybe.Maybe ShortHashKey
   hexTextToShortHashKey :: Data.Text.Internal.Text -> GHC.Maybe.Maybe ShortHashKey
   newKey :: GHC.Types.IO ShortHashKey
@@ -225,7 +225,7 @@ module Sel.SecretKey.Authentication where
   newtype AuthenticationKey = Sel.SecretKey.Authentication.AuthenticationKey (GHC.ForeignPtr.ForeignPtr Foreign.C.Types.CUChar)
   type AuthenticationTag :: *
   newtype AuthenticationTag = Sel.SecretKey.Authentication.AuthenticationTag (GHC.ForeignPtr.ForeignPtr Foreign.C.Types.CUChar)
-  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToHexByteString :: AuthenticationTag -> bytestring-0.11.5.3:Data.ByteString.Internal.Type.StrictByteString
diff --git a/sel/test/package-api-9.8.2.txt b/sel/test/package-api-9.8.2.txt
index 2daf65f..0be24f9 100644
--- a/sel/test/package-api-9.8.2.txt
+++ b/sel/test/package-api-9.8.2.txt
@@ -14,7 +14,7 @@ module Sel.HMAC.SHA256 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.HMAC.SHA256.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoAuthHMACSHA256State)
-  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToBinary :: AuthenticationTag -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
@@ -34,7 +34,7 @@ module Sel.HMAC.SHA512 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.HMAC.SHA512.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoAuthHMACSHA512State)
-  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToBinary :: AuthenticationTag -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
@@ -54,7 +54,7 @@ module Sel.HMAC.SHA512_256 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.HMAC.SHA512_256.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoAuthHMACSHA512256State)
-  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToBinary :: AuthenticationTag -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
@@ -74,7 +74,7 @@ module Sel.Hashing where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.Hashing.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.GenericHashing.CryptoGenericHashState)
-  hashByteString :: GHC.Maybe.Maybe HashKey -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO Hash
+  hashByteString :: GHC.Maybe.Maybe HashKey -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Hash
   hashToBinary :: Hash -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
   hashToHexByteString :: Hash -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
   hashToHexText :: Hash -> Data.Text.Internal.Text
@@ -95,7 +95,7 @@ module Sel.Hashing.Password where
   defaultArgon2Params :: Argon2Params
   genSalt :: GHC.Types.IO Salt
   hashByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO PasswordHash
-  hashByteStringWithParams :: Argon2Params -> Salt -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO PasswordHash
+  hashByteStringWithParams :: Argon2Params -> Salt -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> PasswordHash
   hashText :: Data.Text.Internal.Text -> GHC.Types.IO PasswordHash
   hexByteStringToSalt :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Maybe.Maybe Salt
   hexTextToSalt :: Data.Text.Internal.Text -> GHC.Maybe.Maybe Salt
@@ -115,8 +115,8 @@ module Sel.Hashing.SHA256 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.Hashing.SHA256.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoHashSHA256State)
-  hashByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO Hash
-  hashText :: Data.Text.Internal.Text -> GHC.Types.IO Hash
+  hashByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Hash
+  hashText :: Data.Text.Internal.Text -> Hash
   hashToBinary :: Hash -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
   hashToHexByteString :: Hash -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
   hashToHexText :: Hash -> Data.Text.Internal.Text
@@ -129,8 +129,8 @@ module Sel.Hashing.SHA512 where
   type role Multipart nominal
   type Multipart :: * -> *
   newtype Multipart s = Sel.Hashing.SHA512.Multipart (GHC.Ptr.Ptr LibSodium.Bindings.SHA2.CryptoHashSHA512State)
-  hashByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO Hash
-  hashText :: Data.Text.Internal.Text -> GHC.Types.IO Hash
+  hashByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Hash
+  hashText :: Data.Text.Internal.Text -> Hash
   hashToBinary :: Hash -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
   hashToHexByteString :: Hash -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString
   hashToHexText :: Hash -> Data.Text.Internal.Text
@@ -145,8 +145,8 @@ module Sel.Hashing.Short where
   type ShortHashingException :: *
   data ShortHashingException = ShortHashingException
   binaryToShortHashKey :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Maybe.Maybe ShortHashKey
-  hashByteString :: ShortHashKey -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Types.IO ShortHash
-  hashText :: ShortHashKey -> Data.Text.Internal.Text -> GHC.Types.IO ShortHash
+  hashByteString :: ShortHashKey -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> ShortHash
+  hashText :: ShortHashKey -> Data.Text.Internal.Text -> ShortHash
   hexByteStringToShortHashKey :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> GHC.Maybe.Maybe ShortHashKey
   hexTextToShortHashKey :: Data.Text.Internal.Text -> GHC.Maybe.Maybe ShortHashKey
   newKey :: GHC.Types.IO ShortHashKey
@@ -225,7 +225,7 @@ module Sel.SecretKey.Authentication where
   newtype AuthenticationKey = Sel.SecretKey.Authentication.AuthenticationKey (GHC.ForeignPtr.ForeignPtr Foreign.C.Types.CUChar)
   type AuthenticationTag :: *
   newtype AuthenticationTag = Sel.SecretKey.Authentication.AuthenticationTag (GHC.ForeignPtr.ForeignPtr Foreign.C.Types.CUChar)
-  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> GHC.Types.IO AuthenticationTag
+  authenticate :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> AuthenticationKey -> AuthenticationTag
   authenticationKeyFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationKey
   authenticationTagFromHexByteString :: bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString -> Data.Either.Either Data.Text.Internal.Text AuthenticationTag
   authenticationTagToHexByteString :: AuthenticationTag -> bytestring-0.12.1.0:Data.ByteString.Internal.Type.StrictByteString


Reviews:
user: login: intricate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T21:06:45Z
user: login: Kleidukos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T21:17:35Z
user: login: Kleidukos
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-18T21:23:31Z
user: login: intricate
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T21:56:17Z
user: login: Kleidukos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T05:32:30Z
user: login: Kleidukos
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T05:32:36Z

Review comments:
diff_hunk: @@ -163,8 +163,8 @@ generateKeyPair = do
 -- | Sign a message.
 --
 -- @since 0.0.1.0
-signMessage :: StrictByteString -> SecretKey -> IO SignedMessage
-signMessage message (SecretKey skFPtr) =
+signMessage :: StrictByteString -> SecretKey -> SignedMessage
+signMessage message (SecretKey skFPtr) = unsafeDupablePerformIO $
path: sel/src/Sel/PublicKey/Signature.hs
user: login: intricate
body: This one is a bit questionable because of [this snippet in `libsodium`](https://github.com/jedisct1/libsodium/blob/3e12b517ee42eb4993d19611df325a01104709cc/src/libsodium/crypto_sign/ed25519/ref10/sign.c#L63-L67):

```c
#ifdef ED25519_NONDETERMINISTIC
    _crypto_sign_ed25519_synthetic_r_hv(&hs, nonce /* tmp */, az);
#else
    crypto_hash_sha512_update(&hs, az + 32, 32);
#endif
```

As mentioned in the[ `libsodium` docs](https://doc.libsodium.org/public-key_cryptography/public-key_signatures#notes), non-deterministic `Ed25519` signatures are non-standard, so it's doubtful that most people are building the library with this macro defined.

However, it's totally possible that it is defined. So, unfortunately, maybe it isn't safe to define this as pure :disappointed: 
created_at: 2025-05-18T21:06:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,8 +163,8 @@ generateKeyPair = do
 -- | Sign a message.
 --
 -- @since 0.0.1.0
-signMessage :: StrictByteString -> SecretKey -> IO SignedMessage
-signMessage message (SecretKey skFPtr) =
+signMessage :: StrictByteString -> SecretKey -> SignedMessage
+signMessage message (SecretKey skFPtr) = unsafeDupablePerformIO $
path: sel/src/Sel/PublicKey/Signature.hs
user: login: Kleidukos
body: Hmm yeah, maybe let's keep that one with `IO`, and we'll indicate that in the absence of `ED25519_NONDETERMINISTIC` it's ± safe to use `unsafeDupablePerformIO`
created_at: 2025-05-18T21:17:34Z
author_association: MEMBER
diff_hunk: @@ -162,6 +162,15 @@ generateKeyPair = do
 
 -- | Sign a message.
 --
+-- Note that, if @libsodium@ is compiled with the @ED25519_NONDETERMINISTIC@
+-- macro defined, this function will produce non-deterministic (but also
+-- non-standard) Ed25519 signatures. If @libsodium@ hasn't been compiled with
+-- the @ED25519_NONDETERMINISTIC@ macro defined, it's safe to call this
+-- function in a pure context with 'unsafeDupablePerformIO'.
+--
+-- For more information, see the
+-- [@libsodium@ docs](https://doc.libsodium.org/public-key_cryptography/public-key_signatures#notes).
+--
path: sel/src/Sel/PublicKey/Signature.hs
user: login: intricate
body: @Kleidukos: how do you feel about this comment?
created_at: 2025-05-18T21:56:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -162,6 +162,15 @@ generateKeyPair = do
 
 -- | Sign a message.
 --
+-- Note that, if @libsodium@ is compiled with the @ED25519_NONDETERMINISTIC@
+-- macro defined, this function will produce non-deterministic (but also
+-- non-standard) Ed25519 signatures. If @libsodium@ hasn't been compiled with
+-- the @ED25519_NONDETERMINISTIC@ macro defined, it's safe to call this
+-- function in a pure context with 'unsafeDupablePerformIO'.
+--
+-- For more information, see the
+-- [@libsodium@ docs](https://doc.libsodium.org/public-key_cryptography/public-key_signatures#notes).
+--
path: sel/src/Sel/PublicKey/Signature.hs
user: login: Kleidukos
body: Excellent.
created_at: 2025-05-19T05:32:30Z
author_association: MEMBER

PR comments:
user: login: Kleidukos
created_at: 2025-05-18T21:23:46Z
body: @intricate I'll merge when CI is green, good job!
author_association: MEMBER
user: login: intricate
created_at: 2025-05-18T22:39:49Z
body: _Deleted my previous comment to avoid muddying the waters with a non-issue that I brought up :grin:_

---

Outside of `Sel.PublicKey.Signature.signMessage`, it seems safe to define all of these functions as pure. 

As we know, `libsodium` makes heavy use of "out" parameters, so technically none of this stuff is _truly_ pure. However, apart from mutating buffers and whatnot (of course, **not** mutating any of the _"in"_ parameters), there doesn't appear to be anything else that's secretly stateful. So I think it's probably safe to just call these functions pure.
author_association: CONTRIBUTOR
user: login: Kleidukos
created_at: 2025-05-19T05:34:03Z
body: @intricate Thank you very much for you recent contributions. I believe this was the last PR needed to release a new major version of `sel`. :)
author_association: MEMBER