Files
Evgeny 9f9b6c8e88 crypto: BBS scheme for anonymous credentials with multiple presentations (#1794)
* crypto: BBS scheme for anonymous credentials with multiple presentations

* verify

* add files to sources

* more files

* more files, use cabal 3.0

* fix path

* extensions

* switch libbbs to fork

* return either from keygen

* use only secret key to sign

* improve FFI

* simplify

* update libbbs to support iOS

* add commoncrypto flag

* bump libbbs

* reject input of wrong length

* ci: get submodules

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-06-15 09:44:11 +01:00

392 lines
16 KiB
Haskell

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module CoreTests.CryptoTests (cryptoTests) where
import Control.Concurrent.STM
import Control.Monad.Except
import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Either (isRight)
import Data.Int (Int64)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LE
import Data.Type.Equality
import qualified Data.X509 as X
import qualified Data.X509.CertificateStore as XS
import qualified Data.X509.Validation as XV
import qualified SMPClient
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Crypto.BBS
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
import Simplex.Messaging.Transport.Client
import Test.Hspec hiding (fit, it)
import Test.Hspec.QuickCheck (modifyMaxSuccess)
import Test.QuickCheck
import Util
cryptoTests :: Spec
cryptoTests = do
modifyMaxSuccess (const 10000) . describe "padding / unpadding" $ do
it "should pad / unpad string" . property $ \(s, paddedLen) ->
let b = encodeUtf8 $ T.pack s
len = B.length b
padded = C.pad b paddedLen
in if len < 2 ^ (16 :: Int) - 3 && len <= paddedLen - 2
then (padded >>= C.unPad) == Right b
else padded == Left C.CryptoLargeMsgError
it "pad should fail on large string" $ do
C.pad "abc" 5 `shouldBe` Right "\000\003abc"
C.pad "abc" 4 `shouldBe` Left C.CryptoLargeMsgError
let s = B.replicate 65533 'a'
(C.pad s 65535 >>= C.unPad) `shouldBe` Right s
C.pad (B.replicate 65534 'a') 65536 `shouldBe` Left C.CryptoLargeMsgError
C.pad (B.replicate 65535 'a') 65537 `shouldBe` Left C.CryptoLargeMsgError
it "unpad should fail on invalid string" $ do
C.unPad "\000\000" `shouldBe` Right ""
C.unPad "\000" `shouldBe` Left C.CryptoInvalidMsgError
C.unPad "" `shouldBe` Left C.CryptoInvalidMsgError
it "unpad should fail on shorter string" $ do
C.unPad "\000\003abc" `shouldBe` Right "abc"
C.unPad "\000\003ab" `shouldBe` Left C.CryptoInvalidMsgError
modifyMaxSuccess (const 10000) . describe "lazy padding / unpadding" $ do
it "should pad / unpad lazy bytestrings" . property $ \(s, paddedLen) ->
let b = LE.encodeUtf8 $ LT.pack s
len = LB.length b
padded = LC.pad b len paddedLen
in if len <= paddedLen - 8
then (padded >>= LC.unPad) == Right b
else padded == Left C.CryptoLargeMsgError
it "pad should support large string" $ do
LC.pad "abc" 3 11 `shouldBe` Right "\000\000\000\000\000\000\000\003abc"
LC.pad "abc" 3 10 `shouldBe` Left C.CryptoLargeMsgError
let s = LB.replicate 100000 'a'
(LC.pad s 100000 100100 >>= LC.unPad) `shouldBe` Right s
(LC.pad s 100000 100008 >>= LC.unPad) `shouldBe` Right s
(LC.pad s 100000 100007 >>= LC.unPad) `shouldBe` Left C.CryptoLargeMsgError
it "pad should truncate string if a shorter length is passed and unpad incorrectly when longer length is passed" $ do
let s = LB.replicate 10000 'a'
(LC.pad s 9000 10100 >>= LC.unPad) `shouldBe` Right (LB.take 9000 s)
(LC.pad s 11000 11100 >>= LC.unPad) `shouldBe` Right (s <> LB.replicate 92 '#') -- 92 = pad size, it is not truncated in this case
it "unpad should fail on invalid string" $ do
LC.unPad "\000\000\000\000\000\000\000\000" `shouldBe` Right ""
LC.unPad "\000\000" `shouldBe` Left C.CryptoInvalidMsgError
LC.unPad "" `shouldBe` Left C.CryptoInvalidMsgError
it "unpad won't fail on shorter string" $ do
LC.unPad "\000\000\000\000\000\000\000\003abc" `shouldBe` Right "abc"
LC.unPad "\000\000\000\000\000\000\000\003ab" `shouldBe` Right "ab"
it "should pad / unpad file" testPadUnpadFile
describe "Ed signatures" $ do
describe "Ed25519" $ testSignature C.SEd25519
describe "Ed448" $ testSignature C.SEd448
describe "DH X25519 + cryptobox" testDHCryptoBox
describe "secretbox" testSecretBox
describe "lazy secretbox" $ do
testLazySecretBox
testLazySecretBoxFile
testLazySecretBoxTailTag
testLazySecretBoxFileTailTag
describe "AES GCM" $ do
testAESGCM
describe "X509 key encoding" $ do
describe "Ed25519" $ testEncoding C.SEd25519
describe "Ed448" $ testEncoding C.SEd448
describe "X25519" $ testEncoding C.SX25519
describe "X448" $ testEncoding C.SX448
describe "X509 chains" $ do
it "should validate certificates" testValidateX509
describe "sntrup761" $
it "should enc/dec key" testSNTRUP761
describe "BBS+" $ do
it "should sign and verify" testBBSSignVerify
it "should derive public key from secret key" testBBSPublicKeyDerivation
it "should generate and verify proof" testBBSProofRoundtrip
it "should reject tampered proof" testBBSTamperedProof
it "should reject wrong disclosed message" testBBSWrongMessage
it "should reject wrong public key" testBBSWrongKey
it "should produce unlinkable proofs" testBBSUnlinkable
it "should produce proof of expected size" testBBSProofSize
it "should roundtrip JSON and reject wrong-length input" testBBSJSON
instance Eq C.APublicKey where
C.APublicKey a k == C.APublicKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
instance Eq C.APrivateKey where
C.APrivateKey a k == C.APrivateKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
testPadUnpadFile :: IO ()
testPadUnpadFile = do
let f = "tests/tmp/testpad"
paddedLen = 1024 * 1024
len = 1000000
s = LB.replicate len 'a'
Right s' <- pure $ LC.pad s len paddedLen
LB.writeFile (f <> ".padded") s'
Right s'' <- LC.unPad <$> LB.readFile (f <> ".padded")
s'' `shouldBe` s
testSignature :: (C.AlgorithmI a, C.SignatureAlgorithm a) => C.SAlgorithm a -> Spec
testSignature alg = it "should sign / verify string" . ioProperty $ do
g <- C.newRandom
(k, pk) <- atomically $ C.generateSignatureKeyPair alg g
pure $ \s -> let b = encodeUtf8 $ T.pack s in C.verify k (C.sign pk b) b
testDHCryptoBox :: Spec
testDHCryptoBox = it "should encrypt / decrypt string with asymmetric DH keys" . ioProperty $ do
g <- C.newRandom
(sk, spk) <- atomically $ C.generateKeyPair g
(rk, rpk) <- atomically $ C.generateKeyPair g
nonce <- atomically $ C.randomCbNonce g
pure $ \(s, pad) ->
let b = encodeUtf8 $ T.pack s
paddedLen = B.length b + abs pad + 2
cipher = C.cbEncrypt (C.dh' rk spk) nonce b paddedLen
plain = C.cbDecrypt (C.dh' sk rpk) nonce =<< cipher
in isRight cipher && cipher /= plain && Right b == plain
testSecretBox :: Spec
testSecretBox = it "should encrypt / decrypt string with a random symmetric key" . ioProperty $ do
g <- C.newRandom
k <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
pure $ \(s, pad) ->
let b = encodeUtf8 $ T.pack s
pad' = min (abs pad) 100000
paddedLen = B.length b + pad' + 2
cipher = C.sbEncrypt k nonce b paddedLen
plain = C.sbDecrypt k nonce =<< cipher
in isRight cipher && cipher /= plain && Right b == plain
testLazySecretBox :: Spec
testLazySecretBox = it "should lazily encrypt / decrypt string with a random symmetric key" . ioProperty $ do
g <- C.newRandom
k <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
pure $ \(s, pad) ->
let b = LE.encodeUtf8 $ LT.pack s
len = LB.length b
pad' = min (abs pad) 100000
paddedLen = len + pad' + 8
cipher = LC.sbEncrypt k nonce b len paddedLen
plain = LC.sbDecrypt k nonce =<< cipher
in isRight cipher && cipher /= plain && Right b == plain
testLazySecretBoxFile :: Spec
testLazySecretBoxFile = it "should lazily encrypt / decrypt file with a random symmetric key" $ do
g <- C.newRandom
k <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
let f = "tests/tmp/testsecretbox"
paddedLen = 4 * 1024 * 1024
len = 4 * 1000 * 1000 :: Int64
s = LC.fastReplicate len 'a'
Right s' <- pure $ LC.sbEncrypt k nonce s len paddedLen
LB.writeFile (f <> ".encrypted") s'
Right s'' <- LC.sbDecrypt k nonce <$> LB.readFile (f <> ".encrypted")
s'' `shouldBe` s
testLazySecretBoxTailTag :: Spec
testLazySecretBoxTailTag = it "should lazily encrypt / decrypt string with a random symmetric key (tail tag)" . ioProperty $ do
g <- C.newRandom
k <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
pure $ \(s, pad) ->
let b = LE.encodeUtf8 $ LT.pack s
len = LB.length b
pad' = min (abs pad) 100000
paddedLen = len + pad' + 8
cipher = LC.sbEncryptTailTag k nonce b len paddedLen
plain = LC.sbDecryptTailTag k nonce paddedLen =<< cipher
in isRight cipher && cipher /= (snd <$> plain) && Right (True, b) == plain
testLazySecretBoxFileTailTag :: Spec
testLazySecretBoxFileTailTag = it "should lazily encrypt / decrypt file with a random symmetric key (tail tag)" $ do
g <- C.newRandom
k <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
let f = "tests/tmp/testsecretbox"
paddedLen = 4 * 1024 * 1024
len = 4 * 1000 * 1000 :: Int64
s = LC.fastReplicate len 'a'
Right s' <- pure $ LC.sbEncryptTailTag k nonce s len paddedLen
LB.writeFile (f <> ".encrypted") s'
Right (auth, s'') <- LC.sbDecryptTailTag k nonce paddedLen <$> LB.readFile (f <> ".encrypted")
s'' `shouldBe` s
auth `shouldBe` True
testAESGCM :: Spec
testAESGCM = it "should encrypt / decrypt string with a random symmetric key" $ do
g <- C.newRandom
k <- atomically $ C.randomAesKey g
iv <- atomically $ C.randomGCMIV g
s <- atomically $ C.randomBytes 100 g
Right (tag, cipher) <- runExceptT $ C.encryptAESNoPad k iv s
Right plain <- runExceptT $ C.decryptAESNoPad k iv cipher tag
cipher `shouldNotBe` plain
s `shouldBe` plain
testEncoding :: C.AlgorithmI a => C.SAlgorithm a -> Spec
testEncoding alg = it "should encode / decode key" . ioProperty $ do
g <- C.newRandom
(k, pk) <- atomically $ C.generateAKeyPair alg g
pure $ \(_ :: Int) ->
C.decodePubKey (C.encodePubKey k) == Right k
&& C.decodePrivKey (C.encodePrivKey pk) == Right pk
testValidateX509 :: IO ()
testValidateX509 = do
let checkChain = validateCertificateChain SMPClient.testKeyHash "localhost" "5223" . X.CertificateChain
checkChain [] `shouldReturn` [XV.EmptyChain]
caCreds <- XS.readCertificates "tests/fixtures/ca.crt"
caCreds `shouldNotBe` []
let ca = head caCreds
serverCreds <- XS.readCertificates "tests/fixtures/server.crt"
serverCreds `shouldNotBe` []
let server = head serverCreds
checkChain [server, ca] `shouldReturn` []
ca2Creds <- XS.readCertificates "tests/fixtures/ca2.crt"
ca2Creds `shouldNotBe` []
let ca2 = head ca2Creds
-- signed by another CA
server2Creds <- XS.readCertificates "tests/fixtures/server2.crt"
server2Creds `shouldNotBe` []
let server2 = head server2Creds
checkChain [server2, ca2] `shouldReturn` [XV.UnknownCA]
-- messed up key rotation or other configuration problems
checkChain [server2, ca] `shouldReturn` [XV.InvalidSignature XV.SignatureInvalid]
-- self-signed, unrelated to CA
ssCreds <- XS.readCertificates "tests/fixtures/ss.crt"
ssCreds `shouldNotBe` []
let ss = head ssCreds
checkChain [ss, ca] `shouldReturn` [XV.SelfSigned]
testSNTRUP761 :: IO ()
testSNTRUP761 = do
drg <- C.newRandom
(pk, sk) <- sntrup761Keypair drg
(c, KEMSharedKey k) <- sntrup761Enc drg pk
KEMSharedKey k' <- sntrup761Dec c sk
k' `shouldBe` k
-- BBS+ tests
bbsHeader :: BBSHeader
bbsHeader = BBSHeader "SimpleX"
bbsMessages :: [B.ByteString]
bbsMessages = ["secret_master_key", "2026-07-31", "supporter"]
bbsDisclosedIdxs :: [Int]
bbsDisclosedIdxs = [1, 2]
bbsDisclosedMsgs :: [B.ByteString]
bbsDisclosedMsgs = ["2026-07-31", "supporter"]
testBBSSignVerify :: IO ()
testBBSSignVerify = do
Right (pk, sk) <- bbsKeyGen
let BBSSecretKey skBs = sk
BBSPublicKey pkBs = pk
B.length skBs `shouldBe` 32
B.length pkBs `shouldBe` 96
Right sig <- bbsSign sk bbsHeader bbsMessages
let BBSSignature sigBs = sig
B.length sigBs `shouldBe` 80
bbsVerify pk sig bbsHeader bbsMessages >>= (`shouldBe` True)
bbsVerify pk sig bbsHeader ["wrong", "2026-07-31", "supporter"] >>= (`shouldBe` False)
testBBSPublicKeyDerivation :: IO ()
testBBSPublicKeyDerivation = do
Right (pk, sk) <- bbsKeyGen
-- the public key derived from the secret key matches the one keygen returned
bbsPublicKey sk >>= (`shouldBe` Right pk)
testBBSProofRoundtrip :: IO ()
testBBSProofRoundtrip = do
Right (pk, sk) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph = BBSPresHeader "test-nonce-1"
Right proof <- bbsProofGen pk sig bbsHeader ph bbsDisclosedIdxs bbsMessages
result <- bbsProofVerify pk proof bbsHeader ph bbsDisclosedIdxs 3 bbsDisclosedMsgs
result `shouldBe` True
testBBSTamperedProof :: IO ()
testBBSTamperedProof = do
Right (pk, sk) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph = BBSPresHeader "test-nonce-2"
Right (BBSProof proofBs) <- bbsProofGen pk sig bbsHeader ph bbsDisclosedIdxs bbsMessages
let tampered = BBSProof $ B.take 10 proofBs <> "\xff" <> B.drop 11 proofBs
result <- bbsProofVerify pk tampered bbsHeader ph bbsDisclosedIdxs 3 bbsDisclosedMsgs
result `shouldBe` False
testBBSWrongMessage :: IO ()
testBBSWrongMessage = do
Right (pk, sk) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph = BBSPresHeader "test-nonce-3"
Right proof <- bbsProofGen pk sig bbsHeader ph bbsDisclosedIdxs bbsMessages
result <- bbsProofVerify pk proof bbsHeader ph bbsDisclosedIdxs 3 ["2026-07-31", "business"]
result `shouldBe` False
testBBSWrongKey :: IO ()
testBBSWrongKey = do
Right (pk, sk) <- bbsKeyGen
Right (pk2, _) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph = BBSPresHeader "test-nonce-4"
Right proof <- bbsProofGen pk sig bbsHeader ph bbsDisclosedIdxs bbsMessages
result <- bbsProofVerify pk2 proof bbsHeader ph bbsDisclosedIdxs 3 bbsDisclosedMsgs
result `shouldBe` False
testBBSUnlinkable :: IO ()
testBBSUnlinkable = do
Right (pk, sk) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph1 = BBSPresHeader "nonce-contact-1"
ph2 = BBSPresHeader "nonce-contact-2"
Right (BBSProof proof1) <- bbsProofGen pk sig bbsHeader ph1 bbsDisclosedIdxs bbsMessages
Right (BBSProof proof2) <- bbsProofGen pk sig bbsHeader ph2 bbsDisclosedIdxs bbsMessages
proof1 `shouldNotBe` proof2
bbsProofVerify pk (BBSProof proof1) bbsHeader ph1 bbsDisclosedIdxs 3 bbsDisclosedMsgs >>= (`shouldBe` True)
bbsProofVerify pk (BBSProof proof2) bbsHeader ph2 bbsDisclosedIdxs 3 bbsDisclosedMsgs >>= (`shouldBe` True)
testBBSProofSize :: IO ()
testBBSProofSize = do
Right (pk, sk) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph = BBSPresHeader "test-nonce-size"
Right (BBSProof proofBs) <- bbsProofGen pk sig bbsHeader ph bbsDisclosedIdxs bbsMessages
B.length proofBs `shouldBe` 304 -- 272 + 32 * 1 undisclosed
testBBSJSON :: IO ()
testBBSJSON = do
Right (pk, sk) <- bbsKeyGen
Right sig <- bbsSign sk bbsHeader bbsMessages
let ph = BBSPresHeader "json-nonce"
Right proof <- bbsProofGen pk sig bbsHeader ph bbsDisclosedIdxs bbsMessages
-- valid values roundtrip through JSON
J.decode (J.encode sk) `shouldBe` Just sk
J.decode (J.encode pk) `shouldBe` Just pk
J.decode (J.encode sig) `shouldBe` Just sig
J.decode (J.encode proof) `shouldBe` Just proof
-- FromJSON must reject wrong-length input (regression: StrJSON length validation)
(J.decode (J.encode (BBSSecretKey (B.replicate 16 '\0'))) :: Maybe BBSSecretKey) `shouldBe` Nothing
(J.decode (J.encode (BBSSignature (B.replicate 10 '\0'))) :: Maybe BBSSignature) `shouldBe` Nothing