Developers integrating with contracts/bbs_plus_v1 who need to issue BBS+-signed credentials, or build a holder/verifier flow around selective disclosure, but don't yet have a working mental model of what BBS+ actually does. For the design rationale (why BBS+ over alternatives), see ADR-007. For the raw API surface, read contracts/bbs_plus_v1/README.md and the module source directly — this tutorial explains the flow, not every parameter.
A BBS+ signature is over a vector of messages, not one blob:
messages = [m1, m2, m3, ..., mL] // e.g. [name, dob, institution, degree, grad_year]
signature = Sign(issuer_secret_key, messages)
Three roles:
- Issuer — holds a
SigningKey(secret scalar) and a correspondingVerifyingKey/public key (a G2 point plus circuit metadata such asmessage_count), and signs the full message vector once at credential-issuance time. - Holder — receives the
Signatureand the messages, and later derives aPresentationProofthat discloses only a chosen subset of the messages, blinding the rest and re-randomizing the signature so it cannot be linked to prior presentations. - Verifier — receives the
PresentationProofplus the disclosed messages, and calls verification against the issuer'sVerifyingKey. It learns nothing about undisclosed messages beyond what the disclosed ones and the proof imply.
The key property: the holder does not need to go back to the issuer to produce a new presentation. Every presentation is derived locally and is unlinkable to every other presentation of the same underlying signature.
use bbs_plus_v1::signature::SigningKey;
let mut rng = rand::thread_rng();
let signing_key = SigningKey::generate(&mut rng);
let verifying_key_point = signing_key.public_key(); // G2 point published to holders/verifiersIn production, SigningKey::generate should be called inside an HSM or a secure enclave — the raw Fr scalar returned by signing_key.scalar() is the entire security of every credential this key ever signs. Never log or serialize the signing key scalar outside of a secured issuance service.
Each credential type (e.g. "Engineering Degree v1") fixes a message count and a domain-separation context:
use bbs_plus_v1::signature::VerifyingKey;
let verifying_key = VerifyingKey::derive(
verifying_key_point,
b"quorumproof:credential:engineering-degree:v1", // context_id — schema domain separator
5, // message_count: name, dob, institution, degree, grad_year
)?;The context_id matters: it prevents a signature valid for one credential schema from being reinterpreted as valid for a different schema that happens to share a message count.
use bbs_plus_v1::signature::BbsSignature;
let messages: [Fr; 5] = [
hash_to_scalar(b"Jane Engineer"),
hash_to_scalar(b"1998-04-12"),
hash_to_scalar(b"Acme Institute of Technology"),
hash_to_scalar(b"BSc Civil Engineering"),
hash_to_scalar(b"2021"),
];
let signature = BbsSignature::sign(&mut rng, &signing_key, &messages)?;
let signature_bytes: [u8; 112] = signature.to_bytes(); // stored alongside the credential recordAttributes are hashed to Fr scalars before signing (BBS+ signs field elements, not raw bytes) — use a fixed, documented hash-to-scalar function per credential schema so issuer and holder never disagree on encoding.
Before deriving any presentation, the holder should confirm the issuer's signature verifies against the full message vector:
let is_valid = BbsSignature::verify(&verifying_key, &signature, &messages)?;
assert!(is_valid, "issuer signature does not verify — do not accept this credential");Disclosing every message is the degenerate case — equivalent in information content to a plain signature, but still gets unlinkability across presentations for free.
use bbs_plus_v1::presentation::BbsPresentation;
let disclosed_indices = [0, 1, 2, 3, 4]; // all five attributes
let proof = BbsPresentation::create_presentation(
&mut rng,
&verifying_key,
&signature,
&messages,
&disclosed_indices,
)?;The common case: a verifier only needs to know the holder is a licensed civil engineer in a jurisdiction, not their name or date of birth.
// Disclose only "degree" (index 3); keep name, dob, institution, grad_year hidden.
let disclosed_indices = [3];
let proof = BbsPresentation::create_presentation(
&mut rng,
&verifying_key,
&signature,
&messages,
&disclosed_indices,
)?;
// The holder sends `proof.to_bytes()` plus the cleartext value of messages[3]
// ("BSc Civil Engineering") to the verifier. Nothing else about the credential
// is transmitted or derivable.A holder can independently derive presentations from different credentials for the same verifier interaction (e.g. "degree" from one BBS+-signed credential and "license active" from another) — each presentation is derived and verified independently; there is no built-in cross-credential linking, which is a deliberate anti-correlation property. If a use case genuinely needs to prove two disclosed attributes come from the same holder, that requires an explicit binding mechanism (e.g. a shared holder-committed pseudonym) layered on top — it is not automatic.
Beyond revealing a raw attribute value, BBS+ presentations can carry zero-knowledge predicate proofs over hidden messages (e.g. "graduation year is before 2015" without revealing the exact year). This is exposed as an extension of PresentationProof — check contracts/bbs_plus_v1/src/presentation.rs for the current predicate support surface before relying on a specific predicate type, since this module is still being built out (see the Phase 1 status table in contracts/bbs_plus_v1/README.md).
use bbs_plus_v1::presentation::BbsPresentation;
let disclosed_messages: &[(usize, Fr)] = &[(3, hash_to_scalar(b"BSc Civil Engineering"))];
let ok = BbsPresentation::verify_presentation(
&verifying_key,
&proof,
disclosed_messages,
)?;
if !ok {
// Reject: either the proof is malformed, was not derived from a valid
// issuer signature, or the disclosed message does not match what the
// holder committed to inside the proof.
}The verifier only ever needs the issuer's VerifyingKey and the disclosed messages — it never contacts the issuer and never sees the hidden attributes.
| Property | What it means in practice |
|---|---|
| Selective disclosure | Only attributes the holder explicitly includes in disclosed_indices are visible to the verifier, in cleartext or as a predicate proof. Everything else stays computationally hidden inside the proof. |
| Unlinkability | Each call to create_presentation re-randomizes the underlying signature commitment. Two presentations derived from the same signature and disclosing the same attributes are still cryptographically unlinkable to each other — an issuer or verifier colluding across sessions cannot tell they came from the same credential. |
| Unforgeability (EUF-CMA) | No one without the issuer's SigningKey can produce a signature (or a presentation that verifies) over a message vector the issuer never signed. |
| Honest-verifier zero-knowledge (HVZK) | The presentation proof reveals nothing about hidden messages beyond what is logically implied by the disclosed ones — a verifier following the protocol learns no extra bits. |
| Non-transitive trust | A verifier's ability to check a presentation does not give it the ability to forge new presentations or impersonate the holder to a third party. |
What BBS+ does not protect against:
- Metadata correlation outside the proof — if the holder always presents from the same network IP, wallet address, or timing pattern, that's a linkability channel BBS+ has no control over.
- Issuer collusion at issuance time — the issuer sees the full message vector when signing; BBS+ protects disclosure to verifiers, not from the issuer itself.
- Revocation checks leaking information — naive revocation lookups (e.g. checking a public revocation list by credential ID) can reintroduce linkability. QuorumProof addresses this with the cryptographic accumulator in
contracts/bbs_plus_v1/src/accumulator.rs, which supports non-membership proofs without revealing which specific credential is being checked.
The Rust examples above use the actual contract crate. The following show the same conceptual flow — message vector in, presentation proof out — via a generic client SDK shape. Treat these as illustrative pseudocode until the corresponding JS/Python SDK bindings ship; check docs/sdk-methods-reference.md for what is currently available.
import { BbsPresentation, hashToScalar } from "@quorumproof/bbs-plus-sdk";
const messages = [
hashToScalar("Jane Engineer"),
hashToScalar("1998-04-12"),
hashToScalar("Acme Institute of Technology"),
hashToScalar("BSc Civil Engineering"),
hashToScalar("2021"),
];
const proof = await BbsPresentation.createPresentation({
verifyingKey,
signature,
messages,
disclosedIndices: [3], // reveal only "degree"
});
await sendToVerifier({ proofBytes: proof.toBytes(), disclosed: { degree: "BSc Civil Engineering" } });from quorumproof_bbs import BbsPresentation, VerifyingKey
verifying_key = VerifyingKey.from_bytes(issuer_vk_bytes)
proof = BbsPresentation.from_bytes(proof_bytes)
ok = BbsPresentation.verify_presentation(
verifying_key,
proof,
disclosed_messages=[(3, hash_to_scalar("BSc Civil Engineering"))],
)
if not ok:
raise ValueError("presentation failed verification")soroban contract invoke \
--id $BBS_PLUS_CONTRACT_ID \
--source verifier-account \
--network mainnet \
-- \
verify_presentation \
--verifying_key_hash $VK_HASH \
--proof_bytes $PROOF_HEX \
--disclosed_messages '[{"index":3,"value":"BSc Civil Engineering"}]'- ADR-007: BBS+ Signatures for Selective Disclosure
contracts/bbs_plus_v1/README.mdcontracts/bbs_plus_v1/src/signature.rs,presentation.rs,accumulator.rs- IETF CFRG BBS Signatures draft
docs/privacy-guide.md— broader privacy model this fits into