# Selective disclosure with BBS

> Issue BBS credentials, derive presentations that reveal only chosen attributes, verify them in Java or on Cardano, and bind them against replay.

Canonical URL: https://zeroj.dev/guides/credentials/bbs/

BBS is a signature scheme built for credentials. An **issuer** signs a list of attributes once. The
**holder** can then show any subset of those attributes to a **verifier**, together with a
zero-knowledge proof that the issuer signed the full set. The hidden attributes stay hidden, and two
presentations of the same credential can't be linked to each other by the proof alone.

Think of a passport where you can black out any lines you like before showing it, and the border
officer can still check the government's stamp.

ZeroJ implements BBS in the opt-in `zeroj-bbs` module, following the IRTF CFRG draft
**`draft-irtf-cfrg-bbs-signatures-10`**. It is a draft, not yet an RFC.

## BBS or a circuit?

| You want to… | Use |
|---|---|
| Reveal some signed attributes exactly as issued ("country = NZ") and hide the rest | **BBS**: no circuit, no trusted setup |
| Prove a *predicate* over a hidden value ("age ≥ 18", "balance > 1000") | A **Groth16 circuit** (see [Prove you're over 18](https://zeroj.dev/tutorials/age-check/)) |
| Prove membership in a set without revealing which member | A circuit with a Merkle proof ([private allowlist](https://zeroj.dev/tutorials/private-allowlist/)) |
| Verify on Cardano with a flexible statement | Groth16. On-chain BBS supports one fixed disclosure shape (see below). |

BBS reveals values; it doesn't compute on them. If the verifier only needs a yes/no answer about a
hidden attribute, you want a circuit.

## Status

| Operation | Status |
|---|---|
| Verification (`Verify`, `ProofVerify`) | **Beta**: vector-tested, not audited, spec is an IRTF draft |
| Issuance and presentation (`KeyGen`, `Sign`, `ProofGen`) | **Beta with caveat**: the default pure-Java provider is not constant-time; prefer the blst provider for issuer keys |

Both BLS12-381 ciphersuites from the draft are implemented: `BLS12381_SHA256` (the default,
`BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_`) and `BLS12381_SHAKE256`. The test suite runs the draft's
official fixture vectors for both, across the pure-Java and blst providers.

## Add the dependency

`zeroj-bbs` is published but sits **outside** `zeroj-bom-core`, so give it an explicit version:

```groovy title="build.gradle"
dependencies {
    implementation platform('org.zeroj:zeroj-bom-core:0.1.0-pre12')
    implementation 'org.zeroj:zeroj-bbs:0.1.0-pre12'   // opt-in, explicit version
    implementation 'org.zeroj:zeroj-blst'                  // optional: native provider for issuers
}
```

```xml title="pom.xml"
<dependency>
  <groupId>org.zeroj</groupId>
  <artifactId>zeroj-bbs</artifactId>
  <version>0.1.0-pre12</version>
</dependency>
```

## Issue, present, verify

This complete program plays all three roles. Each role would normally be a different party.

```java title="BbsQuickstart.java"
import org.zeroj.bbs.BbsCiphersuite;
import org.zeroj.bbs.BbsKeyPair;
import org.zeroj.bbs.BbsPresentation;
import org.zeroj.bbs.BbsPresentationCodec;
import org.zeroj.bbs.BbsPublicKey;
import org.zeroj.bbs.BbsRevealedMessage;
import org.zeroj.bbs.BbsService;
import org.zeroj.bbs.BbsSignature;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;

public class BbsQuickstart {

    public static void main(String[] args) {
        BbsService bbs = BbsService.pureJava();   // draft-10, BLS12-381 SHA-256 ciphersuite

        // ---- Issuer: create a key pair (key material is secret, at least 32 bytes) ----
        byte[] keyMaterial = new byte[32];
        new SecureRandom().nextBytes(keyMaterial);
        BbsKeyPair issuer = bbs.keyPair(keyMaterial, utf8("acme-kyc-issuer-2026"));

        // ---- Issuer: sign five attributes under a credential header ----
        List<byte[]> attributes = List.of(
                utf8("name:Alice Liddell"),     // 0
                utf8("dob:1990-04-01"),         // 1
                utf8("country:NZ"),             // 2
                utf8("kycLevel:verified"),      // 3
                utf8("docId:P1234567"));        // 4
        byte[] header = utf8("acme-kyc-credential-v1");
        BbsSignature signature = bbs.sign(issuer.secretKey(), issuer.publicKey(), attributes, header);

        // ---- Holder: check the credential on receipt ----
        boolean credentialOk = bbs.verify(issuer.publicKey(), signature, attributes, header);

        // ---- Verifier: issue a fresh, single-use challenge ----
        byte[] challenge = new byte[32];
        new SecureRandom().nextBytes(challenge);

        // ---- Holder: reveal only country (2) and kycLevel (3), bound to the challenge ----
        BbsPresentation presentation = bbs.derivePresentation(
                issuer.publicKey(), signature, attributes, header, challenge, new int[]{2, 3});
        byte[] wire = BbsPresentationCodec.encode(presentation);   // send this to the verifier

        // ---- Verifier: decode, check the bindings, then the proof ----
        BbsPresentation received = BbsPresentationCodec.decode(wire);
        BbsPublicKey trustedIssuer = new BbsPublicKey(issuer.publicKey().bytes(), BbsCiphersuite.BLS12381_SHA256);
        boolean freshAndBound = Arrays.equals(received.presentationHeader(), challenge)
                && Arrays.equals(received.header(), header);
        boolean proofOk = bbs.verifyPresentation(trustedIssuer, received);

        System.out.println("credentialOk=" + credentialOk + " freshAndBound=" + freshAndBound + " proofOk=" + proofOk);
        for (BbsRevealedMessage m : received.revealedMessages()) {
            System.out.println("  revealed[" + m.index() + "] = " + new String(m.message(), StandardCharsets.UTF_8));
        }
    }

    private static byte[] utf8(String s) {
        return s.getBytes(StandardCharsets.UTF_8);
    }
}
```

Output:

```text
credentialOk=true freshAndBound=true proofOk=true
  revealed[2] = country:NZ
  revealed[3] = kycLevel:verified
```

A few API details worth knowing:

- **Argument order.** `sign(secretKey, publicKey, messages, header)` and
  `verify(publicKey, signature, messages, header)` put messages before the header. Overloads with
  the draft's order, `(…, header, messages)`, also exist.
- **Disclosed indexes** are zero-based and must be strictly ascending: `new int[]{2, 3}`, not
  `{3, 2}`.
- **Every presentation is fresh.** `derivePresentation` draws new randomness from `SecureRandom`
  each time, so repeated presentations don't share proof bytes.
- **Tampering fails.** Change a revealed message, the header or `ph`, and `verifyPresentation`
  returns `false`.
- **Distribute the issuer key as bytes.** `publicKey.bytes()` on the issuer side,
  `new BbsPublicKey(bytes, BbsCiphersuite.BLS12381_SHA256)` on the verifier side. Verifiers must
  get it from a source they trust, not from the holder.

## Headers and presentation headers

BBS has two "extra data" fields, and they do different jobs:

| | Set by | Signed/proved over | Use it for |
|---|---|---|---|
| **header** | Issuer, at signing | The signature and every presentation | Credential type, schema version, issuer context. Verifiers should expect a specific value. |
| **presentation header (`ph`)** | Holder, at presentation, usually from the verifier's challenge | That one presentation | Binding a presentation to one session or transaction, which prevents replay |

`verifyPresentation(publicKey, presentation)` takes both values **from the presentation itself**.
It proves the holder used *some* `ph`; it does not know which one you expected. Always compare
`presentation.presentationHeader()` with the challenge you issued and consume the challenge, and
compare `presentation.header()` with the credential type you accept. Without that, a captured
presentation can be replayed forever.

On Cardano there is no interactive challenge, so derive `ph` from the transaction instead. For
example, `blake2b_256(spentTxId || outputIndex || recipientPkh)`, recomputed by the validator from
`ScriptContext`. Then a presentation only works for the one UTxO and recipient it was made for.

## Encode attributes carefully

Revealed messages are raw bytes. The proof says "the issuer signed these bytes at these positions",
nothing more.

- **Fix the schema.** Agree on what each index means (0 = name, 2 = country, …) and have
  verifiers check the index of every revealed message, not just its value.
- **Make values unambiguous.** Prefix values with the attribute name, or use a canonical encoding,
  so `"NZ"` in one position can't be confused with another field.
- **Remember what BBS doesn't do.** Expiry, revocation, holder binding (proving the presenter
  is the person the credential was issued to), and issuer trust are application policy.

## Choose a provider

`BbsService.pureJava()` needs no native code. Its secret-scalar operations go through
fixed-schedule code, but it is **not** a full JVM constant-time guarantee. For issuer keys, and
anywhere high-value signing keys or proof randomness live, select the native blst provider. The
API stays the same:

```java
import org.zeroj.bbs.BbsCiphersuite;
import org.zeroj.bbs.BbsService;
import org.zeroj.blst.BlstBls12381Provider;

BbsService issuerService = BbsService.withBlsProvider(
        BbsCiphersuite.BLS12381_SHA256, BlstBls12381Provider.createDefault());
```

Signatures and presentations produced with either provider verify with the other: the key pair
derived from the same key material is identical.

## Verify through the ZeroJ SPI

If your service already routes proofs through ZeroJ's verifier registry, `BbsZkVerifier`
(registered with `ServiceLoader`, descriptor name `bbs-bls12381-java`) verifies a presentation carried in a `ZkProofEnvelope`:

- `proofSystem` = `ProofSystemId.BBS`, `curve` = `CurveId.BLS12_381`;
- proof bytes = `BbsPresentationCodec.encode(presentation)`, proof format
  `bbs-cfrg-draft10-presentation-cbor-v1`;
- `VerificationMaterial` VK bytes = the issuer's public key bytes.

Like every ZeroJ backend, it checks cryptography only. The `ph`/header comparison above is still
yours. See [Verify proofs in Java](https://zeroj.dev/guides/verifying/off-chain/).

## Verify on Cardano

`zeroj-onchain-julc` includes `BbsProofVerify`, a native Plutus V3 implementation of BBS
`ProofVerify`, and `BbsToCardano` in `zeroj-bbs` prepares its inputs off-chain:

```java
import org.zeroj.bbs.cardano.BbsToCardano;

var params = BbsToCardano.verifierParams(issuer.publicKey(), header, attributes.size());  // validator @Params
var proof  = BbsToCardano.onChainProof(presentation);                                    // redeemer fields
```

`verifierParams` bakes the issuer key, the generators and the header-dependent domain into the
validator's parameters, so the script hash pins both the issuer and the credential header.

> **Caution: Fixed profile**
>
> `BbsProofVerify.verify` is unrolled for **one shape: a 5-message credential disclosing indexes 2
> and 3** (hiding 0, 1 and 4). Plutus has no cheap dynamic loop, so other shapes need a different
> unrolling. It uses the SHA-256 ciphersuite and was measured at about 2.44 billion CPU steps and
> 183,509 memory units in the JuLC VM. On-chain BBS is an opt-in, unaudited path for testnets.

Your validator composes `BbsProofVerify` with its own policy: check that the disclosed values match
what the datum requires, that `ph` equals the value recomputed from `ScriptContext`, and that the
payout goes to the intended recipient. The `reusable-kyc` app in
[zeroj-usecases](https://github.com/bloxbean/zeroj-usecases) is a complete worked example. See
[Verify proofs on Cardano](https://zeroj.dev/guides/verifying/on-chain/) for the general validator pattern.

## Next steps

- [Selective disclosure use case](https://zeroj.dev/use-cases/selective-disclosure/)
- [Age & KYC use case](https://zeroj.dev/use-cases/age-and-kyc/)
- [Secure your ZK application](https://zeroj.dev/guides/verifying/application-security/)
- [Groth16, PlonK & BBS](https://zeroj.dev/learn/proof-systems/)
