---
title: BBS+ Signatures
description: Sign a vector of attributes on BLS12-381, then prove possession of the signature while disclosing only the attributes you choose — plus blind signing so the issuer never sees part of what it signs.
sidebar:
  order: 3
  icon: eye-off
---

`signatures/bbs` implements the BBS+ signature scheme from
[eprint 2016/663](https://eprint.iacr.org/2016/663.pdf), section 4.3. A BBS+ signature covers an
ordered **vector** of scalar messages rather than one byte string, and that is the entire point: the
holder of a signature can later produce a zero-knowledge proof that says *"an issuer I can name
signed four attributes; here are attributes 3 and 4; I know the other two but I am not telling
you"*. The verifier learns nothing about the hidden attributes beyond the fact that they were signed.

This is the credential primitive. Reach for it when you are issuing something like a driver's
licence or a KYC attestation and the holder must be able to prove "over 21" to a bar without handing
over a birth date, a licence number, and an address. Do **not** reach for it when you just need to
sign a document — the machinery is heavy, verification runs pairings, and
[BLS](/signatures/bls) or ECDSA does that job far more cheaply.

:::danger[Do not deploy this package — message generators collide]
`MessageGenerators.Get(i)` returns the **same point for every index**. The method copies the internal
state array, writes the index into the copy, and then hashes the *original* — so the index never
reaches the hash. Every `H_i` for `i >= 1` is the identical point `h_0.Hash(state)`; only `Get(0)`
differs, returning `h_0` itself.

The consequence is a trivial forgery: because the signature commits to
`h_0^s · Π H_i^{m_i}` and all `H_i` are equal, the signature depends only on the **sum** of the
messages. Any permutation of the signed vector verifies, and so does any different vector with the
same sum. Verified against this repository:

```
original verifies:         true
permuted verifies:         true   // [3,4,5,6] signature accepted for [6,5,4,3]
same-sum forgery verifies: true   // ...and for [1,2,7,8]
```

Everything below describes the API as written. Nothing below is safe to rely on for
unforgeability or for selective disclosure until `signatures/bbs/message_generators.go` hashes the
mutated local copy. See [security notes](/reference/security).
:::

## Requirements

BBS+ needs a pairing, so it needs a `*curves.PairingCurve`. In practice that means BLS12-381:

```go
import (
	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/signatures/bbs"
)

curve := curves.BLS12381(&curves.PointBls12381G2{})
```

The argument to `curves.BLS12381` chooses which group holds the **public key**. Passing
`&curves.PointBls12381G2{}` puts the key in G2 and signatures in G1 — the layout every test in the
package uses. See [the curve abstraction](/foundations/curves) for what `PairingCurve` provides.

Messages are `curves.Scalar`, not bytes. Convert with `curve.Scalar.Hash([]byte("..."))` for
free-form attributes, or `curve.Scalar.New(n)` for small integers.

## Keys and generators

| Prop | Type | Default | Description |
| - | - | - | - |
| `NewKeys(curve *curves.PairingCurve)?` | `(*PublicKey, *SecretKey, error)` | - | Generates a fresh keypair. Note the public key comes FIRST in the return order. |
| `NewSecretKey(curve *curves.PairingCurve)?` | `(*SecretKey, error)` | - | Just the signing key. |
| `SecretKey.PublicKey()?` | `*PublicKey` | - | Derives the verification key. No error return. |
| `MessageGenerators.Init(w *PublicKey, length int)?` | `(*MessageGenerators, error)` | - | Derives `length` message generators plus the blinding generator h0, deterministically from the public key. Errors only on negative length. |
| `MessageGenerators.Get(i int)?` | `curves.PairingPoint` | - | i <= 0 returns h0, the blinding generator. 1..length return message generators. Out of range returns nil (not an error). Currently broken — see the danger callout. |

Generators are **derived from the public key**, not stored with it. That is what lets one key sign
credentials of any width: you re-`Init` with a different `length` and get a different generator set.
It also means the verifier must `Init` with exactly the same `length` the signer used, or every
generator differs and nothing verifies.

`Get` is one-based for messages: message index `i` in your slice uses generator `Get(i + 1)`, and
`Get(0)` is the blinding generator `h_0`.

## Signing and verifying a full vector

Grounded in `TestSignatureWorks`.

```go sign.go
package main

import (
	"fmt"
	"log"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/signatures/bbs"
)

func main() {
	curve := curves.BLS12381(&curves.PointBls12381G2{})

	pk, sk, err := bbs.NewKeys(curve)
	if err != nil {
		log.Fatal(err)
	}

	// One generator per attribute.
	generators, err := new(bbs.MessageGenerators).Init(pk, 4)
	if err != nil {
		log.Fatal(err)
	}

	msgs := []curves.Scalar{
		curve.Scalar.Hash([]byte("did:key:z6Mk...")),
		curve.Scalar.Hash([]byte("Ada")),
		curve.Scalar.Hash([]byte("Lovelace")),
		curve.Scalar.New(36),
	}

	sig, err := sk.Sign(generators, msgs)
	if err != nil {
		log.Fatal(err)
	}

	// Verify returns error, not bool. nil means valid.
	if err := pk.Verify(sig, generators, msgs); err != nil {
		log.Fatal("invalid signature: ", err)
	}
	fmt.Println("signature valid")
}
```

`Sign` is **deterministic**: the internal `e` and `s` scalars come from a SHAKE256 DRBG seeded with
the secret key, the generators, and the messages. Signing the same vector twice with the same key
produces byte-identical output. There is no `io.Reader` parameter and no nonce to misuse.

`Sign` errors on an empty message slice, on `generators.length < len(msgs)`, and on a zero secret
key. `Verify` additionally rejects an identity public key and an identity signature point.

:::note[`Sign` tolerates a short vector; the proof path does not]
`sk.Sign` only requires `generators.length >= len(msgs)`, so you can sign 3 messages against
4 generators. `NewPokSignature` requires `len(msgs) == generators.length` exactly. Size your
generators to the credential, not to a round number.
:::

## Selective disclosure

This is the flow that makes BBS+ worth its cost. The holder turns their signature into a
`PokSignature`, derives a Fiat-Shamir challenge from a merlin transcript, and emits a
`PokSignatureProof`. The verifier rebuilds the same transcript from the proof and the messages it
was shown, recomputes the challenge, and checks the two match.

1. **Classify every message**

    Build a `[]common.ProofMessage` with exactly one entry per generator, in signing order. Use
    `common.RevealedMessage{Message: m}` for attributes the verifier will see and
    `common.ProofSpecificMessage{Message: m}` for attributes it will not. Use
    `common.SharedBlindingMessage{Message: m, Blinding: b}` only when the same hidden value must be
    linked to another proof (a range proof over the same age, for example).

2. **Commit**

    `NewPokSignature(sig, generators, proofMsgs, reader)` randomises the signature and builds the
    Schnorr commitments. The reader supplies the proof's randomness — pass `crand.Reader`.

3. **Derive the challenge**

    Create a merlin transcript with an application-specific label, feed it
    `pok.GetChallengeContribution(transcript)`, append the verifier's nonce, extract 64 bytes and
    reduce them with `curve.Scalar.SetBytesWide`.

4. **Generate**

    `pok.GenerateProof(challenge)` converts the blinding factors into response scalars and returns
    the `*PokSignatureProof`. Send that, the challenge, the revealed messages, and the nonce.

5. **Verify**

    The verifier calls `pokSig.Verify(revealedMsgs, pk, generators, nonce, challenge, transcript)`
    with a transcript constructed **identically** to the prover's.

Grounded in `TestPokSignatureProofSomeMessagesRevealed`.

```go disclose.go
package main

import (
	crand "crypto/rand"
	"fmt"
	"log"

	"github.com/gtank/merlin"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/signatures/bbs"
	"github.com/sonr-io/crypto/signatures/common"
)

const transcriptLabel = "example.com/credential-presentation/v1"

func main() {
	curve := curves.BLS12381(&curves.PointBls12381G2{})
	pk, sk, err := bbs.NewKeys(curve)
	if err != nil {
		log.Fatal(err)
	}
	generators, err := new(bbs.MessageGenerators).Init(pk, 4)
	if err != nil {
		log.Fatal(err)
	}

	msgs := []curves.Scalar{
		curve.Scalar.New(2), // holder id      — keep hidden
		curve.Scalar.New(3), // date of birth  — keep hidden
		curve.Scalar.New(4), // issuer         — reveal
		curve.Scalar.New(5), // credential type — reveal
	}
	sig, err := sk.Sign(generators, msgs)
	if err != nil {
		log.Fatal(err)
	}

	// ---- holder side ------------------------------------------------------
	// One entry per generator, in signing order.
	proofMsgs := []common.ProofMessage{
		&common.ProofSpecificMessage{Message: msgs[0]},
		&common.ProofSpecificMessage{Message: msgs[1]},
		&common.RevealedMessage{Message: msgs[2]},
		&common.RevealedMessage{Message: msgs[3]},
	}

	pok, err := bbs.NewPokSignature(sig, generators, proofMsgs, crand.Reader)
	if err != nil {
		log.Fatal(err)
	}

	nonce := curve.Scalar.Random(crand.Reader) // supplied by the verifier

	transcript := merlin.NewTranscript(transcriptLabel)
	pok.GetChallengeContribution(transcript)
	transcript.AppendMessage([]byte("nonce"), nonce.Bytes())
	okm := transcript.ExtractBytes([]byte("signature proof of knowledge"), 64)
	challenge, err := curve.Scalar.SetBytesWide(okm)
	if err != nil {
		log.Fatal(err)
	}

	proof, err := pok.GenerateProof(challenge)
	if err != nil {
		log.Fatal(err)
	}

	// ---- verifier side ----------------------------------------------------
	revealed := map[int]curves.Scalar{
		2: msgs[2],
		3: msgs[3],
	}

	vTranscript := merlin.NewTranscript(transcriptLabel) // same label, same order
	ok := proof.Verify(revealed, pk, generators, nonce, challenge, vTranscript)
	fmt.Println("presentation valid:", ok)
}
```

`revealed` is keyed by **zero-based message index**, matching the position in the original `msgs`
slice — not by generator index.

### What `Verify` actually checks, and what `VerifySigPok` does not

`PokSignatureProof.Verify` does two independent things:

1. **`VerifySigPok(pk)`** — a pairing check that the randomised signature is a real signature under
   `pk`. You can call this on its own.
2. **Challenge equality** — it calls `GetChallengeContribution(generators, revealedMsgs, challenge,
   transcript)`, re-extracts 64 bytes from the transcript, and compares the result to the challenge
   you passed in. This is what binds the *revealed messages* to the proof.

Step 2 is why the transcript matters so much. If the verifier reveals a different message set, uses
a different transcript label, or appends the nonce at a different point, the recomputed challenge
differs and `Verify` returns `false`.

:::warning[A transcript mismatch is indistinguishable from a forgery]
Prover and verifier must construct the merlin transcript with the **same label, the same appended
messages, in the same order, with the same domain-separation byte strings**. Any divergence produces
a different challenge and `Verify` returns `false` — with no error, no diagnostic, and nothing to
distinguish it from an actual attack. Put the transcript construction in one shared function that
both sides call. `PokSignatureProof.Verify` returns a bare `bool`; there is no error channel at all.
:::

You can also drive the two halves manually — the test does exactly this to show BBS+ composing with
other sigma protocols that share the transcript:

```go
proof.GetChallengeContribution(generators, revealed, challenge, vTranscript)
// ...other protocols append their contributions to vTranscript here...
vTranscript.AppendMessage([]byte("nonce"), nonce.Bytes())
okm := vTranscript.ExtractBytes([]byte("signature proof of knowledge"), 64)
vChallenge, _ := curve.Scalar.SetBytesWide(okm)

valid := proof.VerifySigPok(pk) && challenge.Cmp(vChallenge) == 0
```

## Blind signing

The dual problem: the *issuer* must sign an attribute it is not allowed to see — a link secret, a
biometric template, a device key. The holder commits to those messages, proves knowledge of the
committed values, and the issuer signs the commitment together with the messages it does know.

1. **Holder commits**

    `NewBlindSignatureContext(curve, hiddenMsgs, generators, nonce, reader)` returns the context to
    send to the issuer **and** a `common.SignatureBlinding` the holder keeps. `hiddenMsgs` is a
    `map[int]curves.Scalar` keyed by zero-based message index.

2. **Issuer verifies the commitment**

    `ctx.Verify(knownIndices, generators, nonce)` checks the holder's proof of knowledge of the
    hidden values, so the issuer is not signing arbitrary garbage. `knownIndices` is the sorted list
    of indices the *issuer* supplies.

3. **Issuer signs**

    `ctx.ToBlindSignature(knownMsgs, sk, generators, nonce)` produces a `*BlindSignature`. It calls
    `Verify` internally, so a bad commitment fails here too.

4. **Holder unblinds**

    `blindSig.ToUnblinded(blinding)` adds the retained blinding factor back into the `s` component,
    yielding an ordinary `*Signature` that verifies against the complete message vector.

Grounded in `TestBlindSignatureContext`.

```go blind.go
package main

import (
	crand "crypto/rand"
	"fmt"
	"log"

	"github.com/sonr-io/crypto/core/curves"
	"github.com/sonr-io/crypto/signatures/bbs"
)

func main() {
	curve := curves.BLS12381(&curves.PointBls12381G2{})
	pk, sk, err := bbs.NewKeys(curve)
	if err != nil {
		log.Fatal(err)
	}
	generators, err := new(bbs.MessageGenerators).Init(pk, 4)
	if err != nil {
		log.Fatal(err)
	}
	nonce := curve.Scalar.Random(crand.Reader)

	// ---- holder: hide message 0 from the issuer ---------------------------
	hidden := map[int]curves.Scalar{
		0: curve.Scalar.Hash([]byte("link-secret")),
	}
	ctx, blinding, err := bbs.NewBlindSignatureContext(curve, hidden, generators, nonce, crand.Reader)
	if err != nil {
		log.Fatal(err)
	}
	// Send ctx (and nonce) to the issuer. Keep `blinding`.

	// ---- issuer: signs only what it knows ---------------------------------
	known := map[int]curves.Scalar{
		1: curve.Scalar.Hash([]byte("firstname")),
		2: curve.Scalar.Hash([]byte("lastname")),
		3: curve.Scalar.Hash([]byte("age")),
	}
	blindSig, err := ctx.ToBlindSignature(known, sk, generators, nonce)
	if err != nil {
		log.Fatal(err)
	}

	// ---- holder: unblind and check ----------------------------------------
	sig := blindSig.ToUnblinded(blinding)

	full := []curves.Scalar{hidden[0], known[1], known[2], known[3]}
	if err := pk.Verify(sig, generators, full); err != nil {
		log.Fatal("unblinded signature invalid: ", err)
	}
	fmt.Println("blind-signed credential valid")
}
```

The issuer never sees `hidden[0]`. It only ever handles `ctx.commitment`, a group element, plus a
Schnorr proof that the holder knows the openings.

:::danger[Lose the blinding factor and the signature is dead]
`ToUnblinded` is the only way to turn a `*BlindSignature` into a verifiable `*Signature`, and it
requires the exact `common.SignatureBlinding` returned alongside the context. That value is random,
is never transmitted, and cannot be recovered from the signature, the context, or the issuer.
Persist it atomically with the blind signature or the credential is unusable and must be re-issued.
:::

:::warning[The index sets must partition the vector]
`hidden` and `known` are both keyed by zero-based message index, and between them they must cover
every position `0..length-1` exactly once. Nothing checks this. An index present in neither map
leaves a generator unaccounted for and the unblinded signature simply fails to verify; an index
present in both silently produces a signature over a value the holder did not intend.
:::

## Serialization

Every type here is a `BinaryMarshaler`, but the wire format does not carry its curve, so the
unmarshalling side needs `Init(curve)` first:

```go
data, err := sig.MarshalBinary()

restored := new(bbs.Signature).Init(curve)
err = restored.UnmarshalBinary(data)
```

The same pattern applies to `PublicKey`, `SecretKey`, `BlindSignature`, `BlindSignatureContext`, and
`PokSignatureProof`. Calling `UnmarshalBinary` on a zero-valued struct dereferences nil fields and
panics.

`BlindSignatureContext.MarshalBinary` writes the commitment point followed by the challenge and one
scalar per proof — `PointSize + (N + 1) * ScalarSize` bytes.

## Caveats

:::danger[Broken message binding]
Restated because it invalidates everything above: `MessageGenerators.Get` makes all generators
equal, so a signature binds only the *sum* of the message scalars.
:::

:::warning[Inconsistent failure signalling]
`PublicKey.Verify` and `BlindSignatureContext.Verify` return `error`. `PokSignatureProof.Verify` and
`VerifySigPok` return `bool`. `MessageGenerators.Get` returns a bare `nil` for an out-of-range index
rather than an error, so a bad index surfaces later as a nil-pointer dereference in whatever point
operation consumes it. Do not assume a uniform idiom across this package.
:::

:::note[Deterministic signing has a privacy consequence]
Because `Sign` derives its randomness from `(sk, generators, msgs)`, re-issuing the identical
credential yields the identical signature bytes. That is convenient for idempotent issuance and bad
for unlinkability if raw signatures ever leave the holder. Present via `PokSignatureProof`, which
re-randomises, rather than by forwarding the signature.
:::

## Related

<CardGroup cols={2}>
  <Card title="Proof toolkit" href="/signatures" icon="wrench">
    `signatures/common` — `ProofMessage`, `ProofCommittedBuilder`, `HmacDrbg`, and the scalar aliases
    this page uses.
  </Card>
  <Card title="Schnorr proofs" href="/zero-knowledge/schnorr" icon="binary">
    The standalone sigma protocol, for composing proofs of discrete-log knowledge alongside a
    BBS+ presentation.
  </Card>
  <Card title="Accumulator" href="/zero-knowledge/accumulator" icon="layers">
    Constant-size set membership on the same pairing curve — the usual companion for revocation.
  </Card>
  <Card title="Security notes" href="/reference/security" icon="shield">
    Every defect found while documenting this library, in one place.
  </Card>
</CardGroup>
