Skip to content

Commit 9e8676b

Browse files
crypto: BBS scheme for anonymous credentials with multiple presentations
1 parent ee2ff40 commit 9e8676b

7 files changed

Lines changed: 458 additions & 0 deletions

File tree

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[submodule "cbits/libbbs"]
2+
path = cbits/libbbs
3+
url = https://github.com/Fraunhofer-AISEC/libbbs.git
4+
[submodule "cbits/blst"]
5+
path = cbits/blst
6+
url = https://github.com/supranational/blst.git

cbits/blst

Submodule blst added at db3defd

cbits/libbbs

Submodule libbbs added at 7fcc6cc

plans/2026-06-01-bbs-bindings.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# BBS+ Bindings for simplexmq
2+
3+
Haskell FFI bindings to libbbs for BBS+ signatures. General-purpose - the module knows nothing about specific applications.
4+
5+
## How BBS+ works
6+
7+
BBS+ signs a fixed list of N messages. Each message is an arbitrary byte array. The signer signs all N messages at once with one signature.
8+
9+
The holder of the signature can then generate a proof that selectively discloses some messages and hides others. The verifier learns the disclosed messages and confirms they were signed by the signer, but learns nothing about the hidden messages. Different proofs from the same signature are unlinkable.
10+
11+
Key constraint: the total number of messages N is fixed at signing time. The verifier must know N. A proof generated from a 3-message signature cannot be verified as a 2-message proof.
12+
13+
## Types
14+
15+
```haskell
16+
newtype BBSSecretKey = BBSSecretKey ByteString -- 32 bytes
17+
newtype BBSPublicKey = BBSPublicKey ByteString -- 96 bytes (BLS12-381 G2 point)
18+
newtype BBSSignature = BBSSignature ByteString -- 80 bytes
19+
newtype BBSProof = BBSProof ByteString -- 272 + 32 * numUndisclosed bytes
20+
newtype BBSHeader = BBSHeader ByteString -- always-disclosed context (e.g. protocol identifier)
21+
newtype BBSPresHeader = BBSPresHeader ByteString -- random nonce for proof unlinkability
22+
```
23+
24+
All newtypes get StrEncoding (base64url), ToJSON/FromJSON (via strToJSON/strParseJSON), Eq, Show.
25+
26+
## Functions
27+
28+
```haskell
29+
bbsKeyGen :: IO (BBSSecretKey, BBSPublicKey)
30+
31+
-- C order: sk, pk, header, messages
32+
bbsSign
33+
:: BBSSecretKey
34+
-> BBSPublicKey
35+
-> BBSHeader -- always-disclosed context
36+
-> [ByteString] -- all N messages
37+
-> IO (Either String BBSSignature)
38+
39+
-- C order: pk, signature, header, presentation_header, disclosed_indexes, messages
40+
bbsProofGen
41+
:: BBSPublicKey
42+
-> BBSSignature
43+
-> BBSHeader -- must match what was signed
44+
-> BBSPresHeader -- random nonce bound into the proof
45+
-> [Int] -- disclosed indexes (0-based)
46+
-> [ByteString] -- all N messages (needed internally, hidden ones not revealed in proof)
47+
-> IO (Either String BBSProof)
48+
49+
-- C order: pk, proof, header, presentation_header, disclosed_indexes, n, messages
50+
bbsProofVerify
51+
:: BBSPublicKey
52+
-> BBSProof
53+
-> BBSHeader -- must match what was signed
54+
-> BBSPresHeader -- must match what was used in bbsProofGen
55+
-> [Int] -- disclosed indexes
56+
-> Int -- total message count N
57+
-> [ByteString] -- disclosed messages only
58+
-> IO Bool
59+
```
60+
61+
## How applications use it
62+
63+
An application defines:
64+
- A message layout: which index means what
65+
- Which indexes are disclosed vs hidden
66+
- How to encode application values as ByteString messages
67+
68+
### Badge example (in simplex-chat, not in this module)
69+
70+
Message layout (always 3 messages):
71+
- Index 0: master secret (32 random bytes) - HIDDEN
72+
- Index 1: expiry (UTF-8 encoded timestamp string) - DISCLOSED
73+
- Index 2: badge type (UTF-8 encoded, e.g. "supporter") - DISCLOSED
74+
75+
Signing (v2, on the server):
76+
```
77+
bbsSign sk pk header [ms, encodeUtf8 "2026-07-31", encodeUtf8 "supporter"]
78+
```
79+
80+
Proof generation (v2, on the client):
81+
```
82+
bbsProofGen pk sig header presHeader [1, 2] [ms, encodeUtf8 "2026-07-31", encodeUtf8 "supporter"]
83+
```
84+
85+
Proof verification (v1, on the recipient):
86+
```
87+
bbsProofVerify pk proof header presHeader 3 [1, 2] [encodeUtf8 "2026-07-31", encodeUtf8 "supporter"]
88+
```
89+
90+
The recipient only sees the proof, presentationHeader, expiry string, and badge type string. They verify these were signed by the server (pk is hardcoded). They never see the master secret.
91+
92+
Expiry is always present as a string. Monthly badges use a date like `"2026-07-31"`, lifetime badges use `"lifetime"`. BBS+ doesn't interpret the bytes - expiry semantics are the application's responsibility. This keeps the message count fixed at 3 for all badge types.
93+
94+
## libbbs C API mapping
95+
96+
```c
97+
int bbs_keygen_full(ciphersuite, sk, pk)
98+
int bbs_sign(ciphersuite, sk, pk, signature, header, header_len, n, messages, message_lens)
99+
int bbs_proof_gen(ciphersuite, pk, signature, proof, header, header_len, presentation_header, presentation_header_len, disclosed_indexes, disclosed_indexes_len, n, messages, message_lens)
100+
int bbs_proof_verify(ciphersuite, pk, proof, proof_len, header, header_len, presentation_header, presentation_header_len, disclosed_indexes, disclosed_indexes_len, n, messages, message_lens)
101+
```
102+
103+
We use `bbs_sha256_ciphersuite`. The header parameter is exposed in all Haskell functions - the application decides what to put there. Tests use `"SimpleX"` as header.
104+
105+
The `presentation_header` parameter is what we call `presentationHeader`.
106+
107+
In `bbs_proof_verify`, the `n` parameter is the total number of messages (not the number of disclosed messages). The `messages` array contains only the disclosed messages, and `disclosed_indexes` maps each to its position in the original message list.
108+
109+
## Build
110+
111+
Submodules in cbits/:
112+
- `cbits/libbbs` - https://github.com/Fraunhofer-AISEC/libbbs
113+
- `cbits/blst` - https://github.com/supranational/blst (libbbs dependency)
114+
115+
C sources in cabal: `cbits/blst/src/server.c`, `cbits/blst/build/assembly.S`, libbbs source files.
116+
Include dirs: `cbits/blst/bindings/`, `cbits/blst/src/`, `cbits/libbbs/include/`, `cbits/libbbs/src/`.
117+
C flags: `-D__BLST_PORTABLE__` for cross-CPU-generation compatibility.
118+
119+
## Tests
120+
121+
- Keygen produces keys of correct size
122+
- Sign + proofGen + proofVerify roundtrip succeeds
123+
- Tampered proof fails verification
124+
- Tampered disclosed message fails verification
125+
- Wrong public key fails verification
126+
- Two proofs from same credential with different nonces both verify
127+
- Proof size matches expected (272 + 32 * numUndisclosed)

simplexmq.cabal

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ library
122122
Simplex.Messaging.Crypto.File
123123
Simplex.Messaging.Crypto.Lazy
124124
Simplex.Messaging.Crypto.Ratchet
125+
Simplex.Messaging.Crypto.BBS
125126
Simplex.Messaging.Crypto.SNTRUP761
126127
Simplex.Messaging.Crypto.SNTRUP761.Bindings
127128
Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
@@ -298,9 +299,22 @@ library
298299
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-home-modules -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2
299300
include-dirs:
300301
cbits
302+
cbits/blst/bindings
303+
cbits/blst/src
304+
cbits/libbbs/include
305+
cbits/libbbs/src
306+
cc-options: -D__BLST_PORTABLE__
301307
c-sources:
302308
cbits/sha512.c
303309
cbits/sntrup761.c
310+
cbits/blst/src/server.c
311+
cbits/blst/build/assembly.S
312+
cbits/libbbs/src/bbs.c
313+
cbits/libbbs/src/bbs_ciphersuites.c
314+
cbits/libbbs/src/bbs_util.c
315+
cbits/libbbs/src/compat-string.c
316+
cbits/libbbs/src/sha256.c
317+
cbits/libbbs/src/shake256.c
304318
extra-libraries:
305319
crypto
306320
build-depends:
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
{-# LANGUAGE DerivingStrategies #-}
2+
{-# LANGUAGE DerivingVia #-}
3+
{-# LANGUAGE ForeignFunctionInterface #-}
4+
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5+
6+
module Simplex.Messaging.Crypto.BBS
7+
( BBSSecretKey (..),
8+
BBSPublicKey (..),
9+
BBSSignature (..),
10+
BBSProof (..),
11+
BBSHeader (..),
12+
BBSPresHeader (..),
13+
bbsKeyGen,
14+
bbsSign,
15+
bbsProofGen,
16+
bbsProofVerify,
17+
) where
18+
19+
import Data.Aeson (FromJSON (..), ToJSON (..))
20+
import Data.ByteString (ByteString)
21+
import qualified Data.ByteString as B
22+
import qualified Data.ByteString.Internal as BI
23+
import Foreign
24+
import Foreign.C
25+
import Simplex.Messaging.Encoding.String
26+
27+
newtype BBSSecretKey = BBSSecretKey ByteString
28+
deriving newtype (Eq, Show, StrEncoding)
29+
30+
instance ToJSON BBSSecretKey where
31+
toJSON = strToJSON
32+
toEncoding = strToJEncoding
33+
34+
instance FromJSON BBSSecretKey where
35+
parseJSON = strParseJSON "BBSSecretKey"
36+
37+
newtype BBSPublicKey = BBSPublicKey ByteString
38+
deriving newtype (Eq, Show, StrEncoding)
39+
deriving (ToJSON, FromJSON) via BBSSecretKey
40+
41+
newtype BBSSignature = BBSSignature ByteString
42+
deriving newtype (Eq, Show, StrEncoding)
43+
deriving (ToJSON, FromJSON) via BBSSecretKey
44+
45+
newtype BBSProof = BBSProof ByteString
46+
deriving newtype (Eq, Show, StrEncoding)
47+
deriving (ToJSON, FromJSON) via BBSSecretKey
48+
49+
newtype BBSHeader = BBSHeader ByteString
50+
deriving newtype (Eq, Show, StrEncoding)
51+
deriving (ToJSON, FromJSON) via BBSSecretKey
52+
53+
newtype BBSPresHeader = BBSPresHeader ByteString
54+
deriving newtype (Eq, Show, StrEncoding)
55+
deriving (ToJSON, FromJSON) via BBSSecretKey
56+
57+
-- Constants
58+
59+
bbsSkLen, bbsPkLen, bbsSigLen, bbsProofBaseLen, bbsProofUdElemLen :: Int
60+
bbsSkLen = 32
61+
bbsPkLen = 96
62+
bbsSigLen = 80
63+
bbsProofBaseLen = 272
64+
bbsProofUdElemLen = 32
65+
66+
bbsProofLen :: Int -> Int
67+
bbsProofLen numUndisclosed = bbsProofBaseLen + numUndisclosed * bbsProofUdElemLen
68+
69+
-- FFI
70+
71+
data BBS_Ciphersuite
72+
73+
foreign import ccall "bbs_keygen_full"
74+
c_bbs_keygen_full :: Ptr BBS_Ciphersuite -> Ptr Word8 -> Ptr Word8 -> IO CInt
75+
76+
foreign import ccall "bbs_sign"
77+
c_bbs_sign ::
78+
Ptr BBS_Ciphersuite ->
79+
Ptr Word8 -> Ptr Word8 -> Ptr Word8 ->
80+
Ptr Word8 -> CSize ->
81+
CSize -> Ptr (Ptr Word8) -> Ptr CSize ->
82+
IO CInt
83+
84+
foreign import ccall "bbs_proof_gen"
85+
c_bbs_proof_gen ::
86+
Ptr BBS_Ciphersuite ->
87+
Ptr Word8 -> Ptr Word8 -> Ptr Word8 ->
88+
Ptr Word8 -> CSize ->
89+
Ptr Word8 -> CSize ->
90+
Ptr CSize -> CSize ->
91+
CSize -> Ptr (Ptr Word8) -> Ptr CSize ->
92+
IO CInt
93+
94+
foreign import ccall "bbs_proof_verify"
95+
c_bbs_proof_verify ::
96+
Ptr BBS_Ciphersuite ->
97+
Ptr Word8 ->
98+
Ptr Word8 -> CSize ->
99+
Ptr Word8 -> CSize ->
100+
Ptr Word8 -> CSize ->
101+
Ptr CSize -> CSize ->
102+
CSize -> Ptr (Ptr Word8) -> Ptr CSize ->
103+
IO CInt
104+
105+
foreign import ccall "&bbs_sha256_ciphersuite"
106+
c_bbs_sha256_ciphersuite :: Ptr (Ptr BBS_Ciphersuite)
107+
108+
getCiphersuite :: IO (Ptr BBS_Ciphersuite)
109+
getCiphersuite = peek c_bbs_sha256_ciphersuite
110+
111+
-- Helpers
112+
113+
withBS :: ByteString -> (Ptr Word8 -> CSize -> IO a) -> IO a
114+
withBS bs f = let (fptr, off, len) = BI.toForeignPtr bs
115+
in withForeignPtr fptr $ \ptr -> f (ptr `plusPtr` off) (fromIntegral len)
116+
117+
withMessages :: [ByteString] -> (Ptr (Ptr Word8) -> Ptr CSize -> CSize -> IO a) -> IO a
118+
withMessages msgs f = do
119+
let n = length msgs
120+
allocaArray n $ \msgsPtr ->
121+
allocaArray n $ \lensPtr -> do
122+
pokeMessages msgs msgsPtr lensPtr 0
123+
f msgsPtr lensPtr (fromIntegral n)
124+
where
125+
pokeMessages [] _ _ _ = pure ()
126+
pokeMessages (m : ms) msgsPtr lensPtr i = do
127+
let (fptr, off, len) = BI.toForeignPtr m
128+
withForeignPtr fptr $ \ptr -> do
129+
pokeElemOff msgsPtr i (ptr `plusPtr` off)
130+
pokeElemOff lensPtr i (fromIntegral len)
131+
pokeMessages ms msgsPtr lensPtr (i + 1)
132+
133+
withIndexes :: [Int] -> (Ptr CSize -> CSize -> IO a) -> IO a
134+
withIndexes idxs f = do
135+
let n = length idxs
136+
allocaArray n $ \ptr -> do
137+
pokeArray ptr (map fromIntegral idxs)
138+
f ptr (fromIntegral n)
139+
140+
-- Public API
141+
142+
bbsKeyGen :: IO (BBSSecretKey, BBSPublicKey)
143+
bbsKeyGen = do
144+
cs <- getCiphersuite
145+
sk <- BI.create bbsSkLen $ \_ -> pure ()
146+
pk <- BI.create bbsPkLen $ \_ -> pure ()
147+
withBS sk $ \skPtr _ ->
148+
withBS pk $ \pkPtr _ -> do
149+
rc <- c_bbs_keygen_full cs skPtr pkPtr
150+
if rc == 0
151+
then pure (BBSSecretKey sk, BBSPublicKey pk)
152+
else error "bbsKeyGen failed"
153+
154+
bbsSign ::
155+
BBSSecretKey ->
156+
BBSPublicKey ->
157+
BBSHeader ->
158+
[ByteString] ->
159+
IO (Either String BBSSignature)
160+
bbsSign (BBSSecretKey sk) (BBSPublicKey pk) (BBSHeader header) msgs = do
161+
cs <- getCiphersuite
162+
sig <- BI.create bbsSigLen $ \_ -> pure ()
163+
withBS sk $ \skPtr _ ->
164+
withBS pk $ \pkPtr _ ->
165+
withBS header $ \hdrPtr hdrLen ->
166+
withBS sig $ \sigPtr _ ->
167+
withMessages msgs $ \msgsPtr lensPtr n -> do
168+
rc <- c_bbs_sign cs skPtr pkPtr sigPtr hdrPtr hdrLen n msgsPtr lensPtr
169+
pure $ if rc == 0
170+
then Right (BBSSignature sig)
171+
else Left "bbsSign failed"
172+
173+
bbsProofGen ::
174+
BBSPublicKey ->
175+
BBSSignature ->
176+
BBSHeader ->
177+
BBSPresHeader ->
178+
[Int] ->
179+
[ByteString] ->
180+
IO (Either String BBSProof)
181+
bbsProofGen (BBSPublicKey pk) (BBSSignature sig) (BBSHeader header) (BBSPresHeader ph) disclosedIdxs msgs = do
182+
cs <- getCiphersuite
183+
let numUndisclosed = length msgs - length disclosedIdxs
184+
proofSz = bbsProofLen numUndisclosed
185+
proof <- BI.create proofSz $ \_ -> pure ()
186+
withBS pk $ \pkPtr _ ->
187+
withBS sig $ \sigPtr _ ->
188+
withBS proof $ \proofPtr _ ->
189+
withBS header $ \hdrPtr hdrLen ->
190+
withBS ph $ \phPtr phLen ->
191+
withIndexes disclosedIdxs $ \idxsPtr idxsLen ->
192+
withMessages msgs $ \msgsPtr lensPtr n -> do
193+
rc <- c_bbs_proof_gen cs pkPtr sigPtr proofPtr hdrPtr hdrLen phPtr phLen idxsPtr idxsLen n msgsPtr lensPtr
194+
pure $ if rc == 0
195+
then Right (BBSProof proof)
196+
else Left "bbsProofGen failed"
197+
198+
bbsProofVerify ::
199+
BBSPublicKey ->
200+
BBSProof ->
201+
BBSHeader ->
202+
BBSPresHeader ->
203+
[Int] ->
204+
Int ->
205+
[ByteString] ->
206+
IO Bool
207+
bbsProofVerify (BBSPublicKey pk) (BBSProof proof) (BBSHeader header) (BBSPresHeader ph) disclosedIdxs numMessages disclosedMsgs = do
208+
cs <- getCiphersuite
209+
withBS pk $ \pkPtr _ ->
210+
withBS proof $ \proofPtr proofLen ->
211+
withBS header $ \hdrPtr hdrLen ->
212+
withBS ph $ \phPtr phLen ->
213+
withIndexes disclosedIdxs $ \idxsPtr idxsLen ->
214+
withMessages disclosedMsgs $ \msgsPtr lensPtr _ -> do
215+
rc <- c_bbs_proof_verify cs pkPtr proofPtr proofLen hdrPtr hdrLen phPtr phLen idxsPtr idxsLen (fromIntegral numMessages) msgsPtr lensPtr
216+
pure (rc == 0)
217+

0 commit comments

Comments
 (0)