bbs

package
v0.7.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: BSD-2-Clause Imports: 6 Imported by: 0

Documentation

Overview

Package bbs is this repo's interface to blind BBS credentials.

A BBS credential is signed once over a list of messages and later presented with only the messages the holder chooses to reveal. Two things distinguish it from the mdoc/SD-JWT paths already here:

  • The wallet participates at ISSUANCE. It commits to messages the issuer never sees, and to the public key of a device-held key binding key, and the issuer signs that commitment. So unlike Longfellow and Vega — which are post-issuance transforms over a credential someone else already signed — an issuer cannot bolt BBS on afterwards.
  • Consequently `vc` needs this on BOTH sides: the issuer verifies a commitment and blind-signs it, the verifier checks a presentation.

Why these are interfaces

The implementation is cgo over zk-cred-bbs's C ABI, behind the `bbsnative` build tag. Everything here is defined in terms of the two interfaces below so that an out-of-process implementation later is a constructor swap rather than a rewrite of every call site — a decision taken deliberately before the call sites exist, since retrofitting a seam is the part that never happens.

Availability

Without the `bbsnative` tag, Native returns an implementation whose every method fails with ErrUnavailable.

The issuer builds with the tag by default, because blind BBS has no pure-Go path and an issuer built without it resolves a `format: jwp` credential configuration, passes every check, and then fails at the signer — wired but dead. It stays statically linked while doing it: `cgo-static` in the Makefile's BUILD_CONFIGS, with `netgo` and `osusergo` restoring the pure-Go DNS and user lookups that turning CGO on would otherwise hand to glibc's NSS.

Every other service stays CGO_ENABLED=0 and fully static, and must — this tag buys a native dependency, and nothing that does not need blind BBS should pay for it. See the repository Makefile's `bbs-native-lib` target and the equivalent reasoning for `zknative`.

Index

Constants

View Source
const (
	SuiteNamePlain   = "plain"
	SuiteNameSchnorr = "schnorr"
)

Wire names for the suites, as they appear in a credential request.

Names rather than the numbers above: the number is an FFI detail, and a request member carrying `1` says nothing to anyone reading a log.

View Source
const MaxMessages = 512

MaxMessages bounds how many messages one credential may carry.

Not a protocol limit — a guard. Message count drives generator derivation and proof size, so an unbounded document would let a caller dictate how much work the issuer does.

The bound on the whole message vector is the native crate's, and has to be: the claim-to-message mapping lives there, so this side cannot count the issuer's own claims without re-deriving that mapping — the exact duplication it was moved into the crate to avoid. What Go checks against this constant is IssueParams.HolderPointers: the one input that arrives already counted and is entirely caller-controlled. That rejects the cheap abuse early, with a message naming the field, and the crate still refuses whatever gets past it.

View Source
const OctetPublicKeyLength = 96

OctetPublicKeyLength is the width of a compressed G2 public key.

View Source
const OctetSecretKeyLength = 32

OctetSecretKeyLength is the width of a serialised BBS secret scalar.

Variables

View Source
var (
	// ErrUnavailable is returned by every operation when the binary was
	// built without the `bbsnative` tag.
	ErrUnavailable = errors.New("bbs: native support not compiled in (build with -tags bbsnative)")

	// ErrVerification is returned when a commitment or proof does not
	// verify.
	//
	// The wrapped message is a coarse discriminator meant for logs — which
	// check failed, and structural facts like a length mismatch. It never
	// contains key material or message contents. **It must not be
	// forwarded to a relying party**: an RP learns "invalid", not where to
	// aim next. Callers surfacing a failure outward should match with
	// errors.Is and emit their own opaque response.
	ErrVerification = errors.New("bbs: verification failed")

	// ErrInternal is returned when the native layer failed for a reason
	// that is not a verdict on the input — today, a panic caught crossing
	// the FFI boundary.
	//
	// Kept distinct from ErrVerification on purpose. "This proof is
	// invalid" and "our prover crashed" call for different responses:
	// the first is a normal outcome to be reported to the caller, the
	// second is an incident. Collapsing them into one error hides
	// breakage inside an expected failure path.
	ErrInternal = errors.New("bbs: native call failed")
)

Functions

func Available

func Available() bool

Available reports whether native BBS support was compiled in. Callers that can degrade (for example, by not advertising BBS credential configurations) should check this rather than discovering it on the first request.

func DecodeCommitment

func DecodeCommitment(s string) ([]byte, error)

DecodeCommitment decodes a base64url-encoded `commitment_with_proof` as it arrives on the wire.

func Issue

func Issue(issuer Issuer, p IssueParams) (string, error)

Issue verifies the holder's commitment and blind-signs it together with the issuer's claims, returning a credential in JWP Compact Serialization.

The commitment is verified before anything is signed — including each authenticator's proof of possession of its key binding key. That check is what stops a holder binding a credential to a key it does not control, so it is not optional and not deferred to presentation time.

Where the claim mapping went

An earlier version of this file derived the message list here, in Go: each JSON leaf became `["<RFC 6901 pointer>",<value>]`, sorted by pointer, under a `LayoutVersion` constant. That is gone, and deliberately so.

A BBS signature covers an ordered list of messages, and the issuer, the wallet and the verifier must derive byte-identical lists from the same claims or nothing verifies — with the failure appearing only as a proof that will not check, pointing at nothing. Three implementations of that mapping is three chances to disagree. It now lives once, in the native crate, which the wallet SDKs and the browser build share; the crate follows draft-bormann-jwp-modular-bbs, where the header's `cmap` names each claim's index and the header is itself the BBS `header` input, so the name-to-position binding is authenticated rather than re-derived.

The `LayoutVersion` constant went with it. Its job — making a layout change a detectable migration rather than a silent verification failure — is done by the header's own `kb` value and `cmap`, which travel with every credential.

func KeyPairMatches

func KeyPairMatches(deriver KeyDeriver, secretKey, publicKey []byte) error

KeyPairMatches reports whether two serialised halves really are a pair.

Worth doing once at startup rather than discovering on the first issuance. A mismatched pair signs perfectly well; what fails is every verification afterwards, reporting only "does not verify" — a failure with nothing in it pointing at the configuration that caused it.

The widths are checked first only so the common misconfiguration (the wrong file, or a PEM where raw octets were expected) gets an error that names the problem rather than one from inside the curve arithmetic. The derivation is what actually decides.

func Native

Native returns the platform implementation. Without the `bbsnative` build tag that is a stub whose every method returns ErrUnavailable.

func ValidateDocumentData

func ValidateDocumentData(data json.RawMessage) error

ValidateDocumentData checks the issuer's own claims against what the credential can actually be built from.

A JSON object, and a non-empty one. The object part is structural - the claims become the credential's claim map. The non-empty part is less obvious and is the native crate's rule, asserted by its own vector tests: a credential with nothing to sign is not a smaller credential, it is not a credential. Checked here so a caller can refuse before paying for a status list entry, rather than learning it from the signer afterwards.

func ValidateHolderPointers

func ValidateHolderPointers(pointers []string) error

ValidateHolderPointers checks a holder pointer list against the rules the credential's claim map depends on, so a caller can refuse a request before paying for it.

The messages are phrased to read after the caller's own field name - `bbs_committed_claims` on the OID4VCI request, `holder_pointers` on the gRPC one - because that is the name whoever has to fix it actually sent. One implementation, because these are correctness rules and not style: each of them describes a credential that cannot be built, and two copies is two chances to disagree about which.

Types

type BlindSigner

type BlindSigner interface {
	// BlindSign verifies the holder's commitment — including each
	// authenticator's proof of possession of its key binding key — and
	// signs it together with the issuer's own messages.
	//
	// A commitment that does not verify is rejected, never signed.
	BlindSign(suite Suite, secretKey, publicKey, commitment, header []byte, messages [][]byte) ([]byte, error)
}

BlindSigner is the issuer's half.

type DisclosedClaim

type DisclosedClaim struct {
	// Pointer is the claim's RFC 6901 pointer within the credential.
	Pointer string `json:"pointer"`
	// Value is the claim's JSON value, so a number stays a number and a
	// string stays quoted.
	Value json.RawMessage `json:"value"`
}

DisclosedClaim is one claim a verifier learned from a presentation.

type Disclosure

type Disclosure uint8

Disclosure is what the holder asked to happen to one message.

const (
	// Disclose reveals the message to the verifier.
	Disclose Disclosure = 0
	// Hide proves knowledge of it without revealing it.
	Hide Disclosure = 1
	// Commit hides it and emits a Pedersen commitment the verifier can
	// carry into a further proof.
	Commit Disclosure = 2
)

type IssueParams

type IssueParams struct {
	// Suite is the key binding construction. Must match what the holder
	// used to build Commitment; a mismatch changes the domain separation
	// and the result verifies against nothing.
	Suite Suite

	// SecretKey and PublicKey are the issuer's BBS key pair. Note this
	// cannot be a pki.Signer or a PKCS#11 key: a BBS secret key is a
	// BLS12-381 scalar consumed inside the signing algebra, not something
	// that signs a digest, and mainstream HSMs do not implement the curve.
	SecretKey []byte
	PublicKey []byte

	// Commitment is the holder's `commitment_with_proof`, carrying the
	// messages the issuer never sees and the key binding public keys,
	// together with proof the holder actually holds those keys.
	Commitment []byte

	// Vct is the SD-JWT VC credential type identifier.
	Vct string

	// DocumentData is the issuer's own claims, as a JSON object.
	DocumentData json.RawMessage

	// HolderPointers names the claims the holder committed to, as RFC 6901
	// pointers. The issuer never sees those values — that is the point —
	// but it must still place them in the message vector, and the count has
	// to match what the holder actually committed. The native side checks
	// that against the commitment and refuses a credential whose header
	// would describe a different message vector than the one being signed.
	HolderPointers []string

	// ExtraHeader, if non-empty, is a JSON object merged into the Issuer
	// Header — `iss`, `iat`, `exp` and the like. It may not restate the
	// parameters the container builds itself.
	ExtraHeader json.RawMessage

	// KeyBinding must agree with whether Commitment carries key binding
	// keys: it selects the message layout a verifier will read under.
	KeyBinding KeyBinding
}

IssueParams is everything an issuer needs to produce a BBS credential.

type Issuer

type Issuer interface {
	// Issue verifies the holder's commitment and returns a finished
	// credential in JWP Compact Serialization.
	Issue(p IssueParams) (string, error)
}

Issuer is the issuer's half at the level of a whole credential, as opposed to BlindSigner's raw algebra over an ordered message list.

Prefer this. The mapping from named claims onto that message list lives in the native crate, shared with the wallet SDKs and the browser, precisely so no two of them can derive it differently — a claim ordered differently produces a credential whose every proof fails, with nothing in the failure pointing at why.

type KeyBinding

type KeyBinding uint32

KeyBinding selects the credential's device-binding layout, and with it which message indices are reserved.

const (
	// NoKeyBinding issues a credential bound to no device key.
	NoKeyBinding KeyBinding = 0
	// SchnorrKeyBinding is this profile's Schnorr-on-BLS12-381-G1 binding.
	SchnorrKeyBinding KeyBinding = 1
)

type KeyDeriver

type KeyDeriver interface {
	// SkToPk returns the 96-octet compressed G2 public key for a 32-octet
	// secret scalar (draft-irtf-cfrg-bbs-signatures-08 §3.4.2).
	SkToPk(secretKey []byte) ([]byte, error)
}

KeyDeriver derives a public key from a secret one.

Separate from Issuer because it is not part of issuing anything: it exists so a holder of a key *pair* can establish that the two halves belong together, which nothing else here can do. A length check confirms the widths and says nothing about the pair, and signing with a mismatched one produces credentials that fail at every relying party reporting only "does not verify" - a failure with nothing in it pointing at the configuration that caused it.

type Presentation

type Presentation struct {
	Vct       string           `json:"vct"`
	Disclosed []DisclosedClaim `json:"disclosed"`
}

Presentation is what a verified presentation revealed. Withheld claims are absent rather than null — the verifier does not learn they were withheld beyond their pointer appearing in the credential's map.

type PresentationVerifier

type PresentationVerifier interface {
	// VerifyPresentation returns what the presentation disclosed, or an
	// error if it does not verify. A non-nil result means the issuer really
	// signed every claim in it.
	VerifyPresentation(suite Suite, presentedJWP string, publicKey []byte) (*Presentation, error)
}

PresentationVerifier is the relying party's half at the level of a whole presentation.

type ProofVerifier

type ProofVerifier interface {
	// VerifyProof returns nil only if the proof is valid for exactly these
	// disclosed messages, this disclosure pattern, these headers and this
	// issuer key.
	//
	// issuerKnownMessages is how many of the credential's messages the
	// issuer supplied itself; the rest were committed by the holder. It is
	// part of what the signature covers, so a wrong value fails to verify
	// rather than being ignored.
	VerifyProof(suite Suite, publicKey, proof, header, presentationHeader []byte,
		issuerKnownMessages int, disclosedMessages [][]byte, disclosures []Disclosure) error
}

ProofVerifier is the relying party's half.

type Suite

type Suite uint32

Suite selects the key binding construction, and with it the domain separation the whole credential is bound to. Getting this wrong does not produce an error — it produces values that verify against nothing.

const (
	// SuitePlain is blind BBS with no device binding.
	SuitePlain Suite = 0
	// SuiteSchnorr is blind BBS with a Schnorr-over-BLS12-381-G1 key
	// binding key, the construction described in zk-cred-bbs's PROFILE.md.
	SuiteSchnorr Suite = 1
)

func ParseSuite

func ParseSuite(name string) (Suite, error)

ParseSuite resolves a wire name to a suite.

The suite selects the domain separation everything is computed under - the api_id, and therefore generator derivation and every hash-to-scalar. Holder and issuer must agree or the commitment verifies against nothing, which is why this is carried explicitly rather than defaulted: a wrong guess is indistinguishable from a corrupt commitment, a wrong issuer key, or a tampered proof.

func (Suite) String

func (s Suite) String() string

String returns the wire name.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL