Internet-Draft credkit Composite Proofs July 2026
Markovski Expires 20 January 2027 [Page]
Workgroup:
Independent
Internet-Draft:
draft-markovski-credkit-composite-proofs-latest
Published:
Intended Status:
Informational
Expires:
Author:
T. Markovski
credkit

credkit: Composite BBS Presentations with Hidden-Message Predicates and Revocation

Abstract

This document describes credkit, an experimental construction that composes multiple IETF BBS proofs of knowledge under a single merged Fiat-Shamir challenge. It adds zero-knowledge set-membership, range, and non-revocation predicates over signed messages without disclosing those messages.

credkit is layered directly on the CFRG BBS Signatures and Blind BBS Signatures drafts and is motivated by the W3C Verifiable Credentials Data Model and the bbs-2023 Data Integrity cryptosuite. For readers already fluent in those specifications, this document identifies what credkit reuses, where it diverges, and how it constructs cross-credential witness equality (a "link secret"), predicates bound to hidden signed values, and a privacy-preserving revocation gate bound to a hidden credential identifier.

This is a description of an experiment, not a standards-track proposal. It defines no new registry, claims no interoperability, and has no formal standing. It exists so the design can be reviewed as a design rather than as source code.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 20 January 2027.

Table of Contents

1. Introduction

The BBS signature scheme [I-D.irtf-cfrg-bbs-signatures] gives holders selective disclosure and verifier unlinkability: each presentation is a freshly randomized proof of knowledge, so two verifiers who collude on the transcripts they received cannot tell they saw the same credential. The Blind BBS extension [I-D.irtf-cfrg-bbs-blind-signatures] lets an issuer sign messages it never learns. This allows a holder-chosen secret to be carried across credentials from different issuers.

Neither specification, on its own, lets a holder prove anything about a message it does not disclose. Selective disclosure is all-or-nothing per message: a birth date is either revealed verbatim or withheld entirely. The bbs-2023 Data Integrity cryptosuite [DI-BBS] inherits this limitation: it discloses or hides whole statements and defines no predicate mechanism, so a deployment that needs one (for example, "older than 18") must add an external construction.

The wallet that motivated this work signed a Poseidon commitment to the birth date into the credential, disclosed that commitment on every presentation, and used a separate circuit to prove that it opened to a date past a cutoff. Because the disclosed field element was fixed and identical across presentations, it became a perfect cross-verifier correlation handle and reintroduced exactly what BBS was chosen to remove.

The flaw was the fixed commitment, not the bridge itself. A fresh per-presentation commitment (as in Blind BBS COMMIT mode or AnonCreds' Pedersen bridge) does not correlate. This was one deployment's approach — the concrete failure credkit started from — not a claim about how the field at large builds predicates.

credkit is an attempt to close that gap without a disclosed commitment and without a SNARK. It keeps the IETF BBS wire format and ciphersuites unchanged at the bottom, and builds three layers on top:

A JSON-LD cryptosuite packages these presentations into W3C Verifiable Credentials [VC-DATA-MODEL] in the manner of bbs-2023. A second cryptosuite carries N such credentials under one challenge in a Verifiable Presentation. Both are implemented and summarized in Section 12; their full specification is out of scope here.

1.1. What this document is and is not

This document is descriptive. It records the choices a single reference implementation makes and why, at the level of detail an independent reader would need to follow the security argument. It is not:

  • a standards-track document, or a candidate to become one;
  • an interoperability target — the constructions above the BBS core have no second implementation and no published test vectors beyond the golden vectors pinned in the reference implementation's own test suite;
  • a claim of novelty for the underlying cryptography, all of which is drawn from the cited literature.

Where this document says an implementation "MUST" or "rejects" something, it is stating what the reference implementation enforces and why that enforcement is load-bearing for soundness or privacy, not imposing a conformance requirement on anyone. Limitations and weaknesses the design knowingly accepts are collected in Section 11.

1.2. Relationship to existing specifications

credkit is deliberately stratified so that each layer's relationship to prior work is clean:

Table 1
Layer Relationship to prior work
BBS core: KeyGen, Sign, Verify, ProofGen, ProofVerify Implements [I-D.irtf-cfrg-bbs-signatures] unchanged; passes its vendored test vectors byte-for-byte.
Blind issuance: Commit, BlindSign Implements the stable, fixture-covered part of [I-D.irtf-cfrg-bbs-blind-signatures]; omits COMMIT-mode disclosure (see Section 3.3).
Numeric messages A narrow extension to the BBS message-to-scalar map (see Section 3.1).
Composite presentations, predicates New; described here. Neither IETF draft has an equivalent.
Accumulator revocation A credkit composition of the positive VB accumulator [VB20], a registry operation with additions held static [KB21], deletion-only public updates, and a CDH weak-BB membership proof bound to a BBS message.
JSON-LD cryptosuite Built for single- and multi-credential presentations; motivated by bbs-2023 [DI-BBS]. Summarized in Section 12, not fully specified here.

The only modifications to the BBS layer are (a) numeric messages and (b) exposing the proof generation algorithm's per-message Schnorr blindings so an outer protocol can reuse them. Both are described precisely in Section 3. Everything else in that layer is the IETF construction as written.

Two adjacent CFRG efforts deserve precise situating. [I-D.irtf-cfrg-sigma-protocols] standardizes interactive sigma protocols proving knowledge of preimages of linear maps in prime-order groups, and [I-D.irtf-cfrg-fiat-shamir] standardizes their non-interactive form via a duplex-sponge transcript.

This document is structurally aligned with both. Each statement is proven in the same commit/challenge/response phases: the split-phase interface of Section 3.2 follows the prover_commit/prover_response decomposition. The transcript of Section 4 enforces the same discipline: labeled prefix-free absorption, protocol and ciphersuite binding, and exactly one challenge per transcript.

This document is not, however, an instantiation of either draft, for three reasons:

  1. AND composition of statements under one challenge — the mechanism this document exists to describe — is explicitly out of scope of [I-D.irtf-cfrg-sigma-protocols].

  2. Challenge derivation here stays in the BBS hash_to_scalar family rather than adopting the Keccak duplex sponge of [I-D.irtf-cfrg-fiat-shamir]. The BBS layer must derive challenges with hash_to_scalar to conform to [I-D.irtf-cfrg-bbs-signatures]. Running a second derivation primitive in the composite layer would create exactly the two-path Fiat-Shamir fork that Section 4.5 exists to prevent.

  3. [I-D.irtf-cfrg-sigma-protocols] currently defines only P-256 normatively. Neither the BBS pairing verification equation nor the GT-valued predicate commitments of Section 6 fall within its linear-map scope.

Should those drafts mature to cover BLS12-381 and statement composition, re-aligning this layer's transcript with them would be a natural future revision.

1.3. Relationship to AnonCreds v2

The construction this document describes is architecturally the same as AnonCreds v2, whose Rust implementation [ANONCREDS-V2] served as the architecture reference for the composite layer. The feature sets coincide almost exactly: multi-message BBS credentials, blind issuance over a holder-chosen link secret, selective disclosure, equality of hidden messages across credentials, predicates over hidden signed values, accumulator-based revocation, and the composition of all of these statements into one presentation. Two of its design decisions are adopted here directly: the Pedersen indirection that decouples a predicate backend from the signature proof (see Section 6.3.2), and the integer message encoding that makes predicates over signed values possible at all (see Section 3.1).

The two systems nonetheless share no bytes at any layer (all observations are as of crate credx 0.2.1):

Table 2
Layer AnonCreds v2 credkit
BBS core A vendored pre-IETF BBS from the academic short-group-signature lineage: its own generator derivation, Merlin transcript labels, no ciphersuite identifiers [I-D.irtf-cfrg-bbs-signatures] unchanged, verified byte-for-byte against its test vectors
Blind issuance Classic BBS+ blind signing — a G1 commitment with a Schnorr proof, predating the IETF draft The Commit/BlindSign flow of [I-D.irtf-cfrg-bbs-blind-signatures]
Predicates Bulletproofs range proofs over 64-bit ranges CCS signed-set membership and digit decomposition [CCS08] (see Section 6)
Non-revocation The same positive VB accumulator [VB20], proven with the paper's own membership protocol (additional generators, prover GT arithmetic) The same accumulator, proven with the CDH weak-BB protocol (see Section 7.3)
Transcript Merlin The labeled transcript of Section 4, kept in the BBS hash_to_scalar family
Envelope A bespoke presentation format W3C Verifiable Credentials and Presentations secured by Data Integrity proofs (see Section 12)

Every intermediate value consequently diverges — generators, domain separation, challenges, wire encodings — so neither implementation can serve as a test oracle for the other, and no interoperability between them is possible or intended. Cross-checking is structural only: the same statement composition, the same equality mechanic, the same protocol shapes. The comparison also runs the other way: AnonCreds v2 provides verifiable encryption of hidden messages toward a designated third party, which credkit does not implement. The relationship is fairly summarized as: the AnonCreds v2 architecture, re-derived over the IETF BBS wire format and packaged as JSON-LD credentials, with the predicate and accumulator-proof slots filled differently.

1.4. Terminology

Holder, Issuer, Verifier, Prover, and the BBS operation names (Sign, Commit, BlindSign, ProofGen, ProofVerify) are used as in [I-D.irtf-cfrg-bbs-signatures] and [I-D.irtf-cfrg-bbs-blind-signatures].

Statement:
One BBS proof of knowledge over one signature within a composite presentation. The Prover holds the full witness (signature, all messages); the Verifier sees only public values (public key, header, disclosed messages).
Presentation:
A set of N statements proven together under one merged Fiat-Shamir challenge, together with any equality constraints and predicates over their hidden messages.
Presentation header:
An application-chosen octet string bound into the merged challenge — the presentation-scope analog of the ph input to BBS ProofGen. There is exactly one per presentation; statements inside a composite presentation carry no per-statement ph. Its intended content is Verifier-supplied freshness and audience data (see Section 9.8).
Predicate:
A set-membership, range, or accumulator-membership claim about a single hidden numeric message: that it lies in a signed set or interval, or remains in an unrevoked registry, without revealing which member or which value.
Alphabet / signed set:
The set of scalars a Verifier has Boneh-Boyen-signed [BB04] to serve as the public parameters of a predicate. For a range predicate it is the digit alphabet {0, ..., u-1}; for set membership it is an arbitrary set of distinct scalars.
Link secret:
A holder-chosen scalar committed at issuance and signed blindly into multiple credentials, so that a later presentation can prove two credentials share it — proving common holdership without an identifier.
Revocation identifier:
A fresh issuer-assigned scalar signed into one credential as a hidden numeric message. It indexes that credential in an accumulator registry but MUST NOT be disclosed or used as a cross- credential equality witness.
Membership witness:
Mutable holder-side state C = (1/(y + alpha)) * V proving that revocation identifier y belongs to accumulator value V. It is kept beside the credential and updated after deletion epochs; it is not part of the signed credential or a proof envelope.
Registry epoch:
A sequential identifier for one published accumulator value and its deletion update. The Verifier states which epoch and accumulator value it accepts.

1.5. Notation and conventions

The key words "MUST", "MUST NOT", "SHOULD", and "MAY" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, with the scope limited to what the reference implementation enforces as described in Section 1.1.

  • G1, G2, GT are the BLS12-381 groups; e: G1 x G2 -> GT is the pairing; r is the prime subgroup order. G1.BASE, G2.BASE are the standard generators from [I-D.irtf-cfrg-bbs-signatures].
  • Scalars are elements of the field of order r, written lowercase (x, v, c). Group elements are uppercase (A, V, Abar). a * P is scalar multiplication; P + Q is the group operation; -P is negation. Arithmetic on scalars is mod r throughout.
  • a || b is octet-string concatenation. I2OSP(n, len) and OS2IP(octets) are as in [RFC8017]. len(x) is the octet length of x.
  • hash_to_scalar(msg, dst) is the BBS operation from [I-D.irtf-cfrg-bbs-signatures].
  • utf8(s) is the UTF-8 encoding of ASCII string s.
  • A Prover's Schnorr blinding for a hidden message is written m~ (drawn per presentation); the corresponding response scalar the Verifier reconstructs is m^ = m~ + c * m, where c is the challenge and m the message scalar. This is the response convention of [I-D.irtf-cfrg-bbs-signatures].

Two ciphersuites are used, unchanged from the BBS drafts: BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_ (SHA-256, via expand_message_xmd [RFC9380]) and BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_ (SHAKE-256, via expand_message_xof). Group elements serialize with the point encoding of the BBS drafts: G1 compressed to 48 octets, G2 compressed to 96 octets, scalars to 32 octets.

2. Architecture overview

2.1. Five properties, one construction

credkit exists because five privacy properties usually treated as separate features can, with BBS plus blind issuance, be realized in a single construction:

  • Verifier unlinkability is already provided by BBS: each proof is freshly randomized. credkit preserves it by never adding a stable, disclosed value to a presentation.
  • Blind issuance lets the Issuer sign a holder-committed message it never sees [I-D.irtf-cfrg-bbs-blind-signatures].
  • Credential linkability is a witness-equality claim: two statements sharing one hidden message's Schnorr blinding under one challenge produce equal response scalars if and only if the messages are equal. If the shared message is a blindly signed link secret, this proves common holdership and nothing else.
  • Predicates are the same shared-blinding idea pointed at a Boneh-Boyen-signed alphabet rather than a second credential.
  • Private non-revocation points that shared blinding at a positive accumulator membership proof. The Verifier learns that a hidden signed revocation identifier remains in the accepted registry state, never the identifier itself.

The load-bearing observation is that credential linkage, predicates, and private non-revocation all reduce to sharing a Schnorr blinding under one Fiat-Shamir challenge. The merged transcript of Section 4 is the single mechanism that makes each of them sound, and using more than one challenge breaks all of them in the same way (see Section 4.5).

2.2. Layering

  +-------------------------------------------------------------+
  |  JSON-LD cryptosuite  (VC + VP envelope; bbs-2023-based)    |
  +-------------------------------------------------------------+
  |  Composite presentations                                    |
  |    - merged Fiat-Shamir transcript                          |
  |    - witness equality (link secret)                         |
  |    - predicate + accumulator binding                        |
  +-----------------------+------------------+------------------+
  | BBS core + blind      | CCS predicates   | VB accumulator   |
  | issuance (IETF, minus | (BB alphabet)    | - static joins   |
  | COMMIT; numeric       | - membership     | - delete epochs  |
  | messages added)       | - range          | - CDH proof      |
  +-----------------------+------------------+------------------+
  |  BLS12-381 pairing (@noble/curves); no WASM, no SNARK       |
  +-------------------------------------------------------------+

The predicate and accumulator layers share exactly one thing with the BBS layer: response scalars in the field of order r. They do not share group elements. This is why their proofs compose with BBS proofs by an algebraic relation between responses rather than through a stable disclosed commitment.

3. Divergences from IETF BBS

The BBS core is used unchanged except for the following three points. All three are necessary for the layers above; none alters the BBS wire format or the results of the vendored test vectors.

3.1. Numeric messages

[I-D.irtf-cfrg-bbs-signatures] maps every message to a scalar by hashing: msg_scalar = hash_to_scalar(msg, api_id || "MAP_MSG_TO_SCALAR_AS_HASH_"). A predicate must do arithmetic on the signed value (compare it to a bound, decompose it into digits), which a hash destroys. credkit therefore admits a second message type: a message MAY be supplied as an integer in [0, r), in which case it is its own scalar, bypassing the hash.

messageToScalar(suite, message):
  if message is an integer:
     if message < 0 or message >= r: reject
     return message
  else:
     return hash_to_scalar(message, api_id || "MAP_MSG_TO_SCALAR_AS_HASH_")

This is the same technique AnonCreds uses for its attribute encoding (see Section 1.3), and it is the only change to the signing path. Bytes messages are unaffected and continue to reproduce the BBS vectors. The consequences for predicate semantics — that predicates are modular, and that honest values must be encoded well below r for the natural comparison to hold — are discussed in Section 9.9. A predicate MUST reference a message that was signed as an integer; a predicate pointed at a hash-mapped (bytes) message is rejected by the Prover, and could not accidentally verify in any case (the hash would have to land in a ~2^-239 window).

3.2. Reachable Schnorr blindings and split-phase proof generation

The composite layer needs to (a) inject a chosen Schnorr blinding into a specific message slot before the challenge is computed, and (b) read back the response scalar for that slot after. The BBS ProofGen of [I-D.irtf-cfrg-bbs-signatures] computes its own challenge internally and exposes neither. credkit therefore factors ProofGen into three phases:

  • ProofInit: draw randomness, compute the first-move commitments (Abar, Bbar, D, T1, T2, domain), retaining the per-message blindings m~;
  • an externally supplied challenge c;
  • ProofFinalize: fold c into responses.

It correspondingly exposes ProofVerifyInit / ProofVerifyFinalize. The single-phase ProofGen/ProofVerify are recovered as the compositions that compute the challenge with the IETF ProofChallengeCalculate, and those compositions still pass the vendored vectors byte-for-byte. Only the challenge source changes when a proof is used inside a presentation.

3.3. No COMMIT mode

Version -03 of [I-D.irtf-cfrg-bbs-blind-signatures] added a third per-message disclosure mode, COMMIT, that discloses a fresh per-presentation commitment C_i = Y_0 * s_i + Y_1 * messages[i] intended as the hook for external predicates. credkit does not implement it, for two reasons. First, it is the least settled part of the draft — no test vectors cover it and its serialization was rewritten during -03. Second, and more importantly, the composite layer makes it redundant: a per-presentation commitment to a signed-but-hidden value, provably opening to the signed message, is exactly what witness equality between a BBS statement and a commitment statement gives, built from parts the link secret already requires. COMMIT mode is the specification's convenience for consumers that lack a composite framework; credkit is a composite framework.

3.4. Deliberate non-interoperability above the core

A composite presentation is not a BBS proof, even when it contains a single statement. Its challenge is derived from credkit's transcript Section 4, not from the IETF ProofChallengeCalculate, so it will not pass ProofVerify. This is intentional: a special-cased "N = 1 is a plain BBS proof" path would be a second challenge-derivation code path, which is precisely the kind of Fiat-Shamir fork that produces soundness bugs. There is one challenge-derivation path. A consumer that needs a wire-compatible BBS proof uses BBS ProofGen directly.

4. The merged Fiat-Shamir transcript

Every soundness property in credkit rests on all statements and predicates in a presentation sharing one challenge derived by hashing one transcript. An ad-hoc H(a || b || c) construction could let a malicious Prover shift octets between fields and forge a proof over a different statement that hashes identically. This is the "Frozen Heart" bug class [FrozenHeart]. The rules below prevent that ambiguity.

The construction follows the same transcript discipline as [I-D.irtf-cfrg-fiat-shamir] — labeled prefix-free absorption, initial protocol binding, one squeeze — but derives the challenge with the BBS hash_to_scalar rather than that draft's Keccak duplex sponge, keeping a single challenge-derivation primitive across the BBS and composite layers (see Section 1.2).

4.1. Absorption

The transcript accumulates labeled, length-framed entries. Absorbing a labeled value appends:

I2OSP(len(label), 8) || label || I2OSP(len(value), 8) || value

Because both the label and the value are length-prefixed, no two distinct sequences of absorbed entries can produce the same octet stream. Labels MUST be non-empty. The typed helpers absorb:

  • a number as I2OSP(n, 8);
  • a scalar as I2OSP(s, 32);
  • a G1 point as its 48-octet compressed encoding;
  • a GT element as its canonical serialization (see Section 4.4);
  • raw octets verbatim.

At construction the transcript first absorbs ("protocol", PROTOCOL_ID) and ("ciphersuite", ciphersuite_id), binding every challenge to the protocol version and the ciphersuite. PROTOCOL_ID for this revision is the ASCII string CREDKIT-PROOFS-V4.

4.2. Layout

A presentation over N statements, with E equality constraints, K range predicates, M set-membership predicates, and A accumulator-membership predicates, absorbs the following sequence. Prover and Verifier MUST absorb it identically; the entire layout is one function used by both sides.

("protocol", "CREDKIT-PROOFS-V4")
("ciphersuite", ciphersuite_id)
("presentation_header", presentation_header)
("statement_count", N)
for s in 0..N-1:
    ("statement", s)
    ("public_key", pk_s)                # G2, 96 octets
    ("header", header_s)
    ("issuer_known_count", L_s)         # messages the issuer knew
    ("total_message_count", L_s + M_s)  # incl. committed messages
    ("disclosed_count", D_s)
    for each disclosed slot j (ascending proof-space index):
        ("disclosed_index", j)
        ("disclosed_scalar", msg_scalar_j)
    ("Abar", Abar_s)  ("Bbar", Bbar_s)  ("D", D_s)
    ("T1", T1_s)      ("T2", T2_s)      ("domain", domain_s)
("equality_constraint_count", E)
for each equality class:
    ("equality_ref_count", |refs|)
    for each ref (statement, messageIndex):
        ("ref_statement", ref.statement)
        ("ref_message_index", ref.messageIndex)
("range_predicate_count", K)
for k in 0..K-1:
    ("predicate", k)
    ("predicate_statement", p_k.statement)
    ("predicate_message_index", p_k.messageIndex)
    ("predicate_kind", utf8(p_k.kind))        # "greaterOrEqual" | "lessOrEqual"
    ("predicate_bound", p_k.bound)            # scalar
    ("predicate_digits", p_k.digits)
    ("predicate_params", rangeParamsToOctets(p_k.params))
    for i in 0..digits-1:
        ("V", V_i)   # G1, blinded alphabet signature for digit i
        ("R", R_i)   # GT, sigma commitment for digit i
("set_membership_count", M)
for k in 0..M-1:
    ("membership", k)
    ("membership_statement", m_k.statement)
    ("membership_message_index", m_k.messageIndex)
    ("membership_params", setParamsToOctets(m_k.params))
    ("V", V_k)   # G1, blinded signature on the hidden member
    ("R", R_k)   # GT, sigma commitment
("accumulator_membership_count", A)
for k in 0..A-1:
    ("accumulator_membership", k)
    ("accumulator_statement", a_k.statement)
    ("accumulator_message_index", a_k.messageIndex)
    ("accumulator_params", accumulatorParamsToOctets(a_k.params))
    ("accumulator_value", a_k.accumulator)  # G1
    ("accumulator_epoch", a_k.epoch)
    ("CPrime", CPrime_k)                    # randomized witness, G1
    ("CBar", CBar_k)                        # alpha relation, G1
    ("T", T_k)                              # sigma commitment, G1

Every field a Verifier must agree on is absorbed, including the full serialized predicate parameters and the predicate ordering. Ordering is therefore significant: the Prover and Verifier MUST supply the same statements, constraints, and predicates in the same order. This is deliberate — canonicalizing the order inside the library would silently paper over a Prover/Verifier disagreement instead of failing it.

Even an empty predicate section absorbs its count (("range_predicate_count", 0), ("set_membership_count", 0), and ("accumulator_membership_count", 0)), so that adding predicates to a presentation cannot collide with a predicate-free presentation over the same statements.

4.3. Challenge derivation

After the full layout is absorbed, one final empty-valued label is absorbed and the challenge is derived:

("presentation_challenge", "")
challenge = hash_to_scalar(
    concat(all absorbed pieces),
    utf8(PROTOCOL_ID || "-" || ciphersuite_id || "H2S_"))

The domain separation tag is specific to both the protocol version and the ciphersuite.

4.4. GT serialization

Predicate sigma commitments live in GT. They are absorbed as the uncompressed Fp12 serialization of the reference implementation's pairing library (@noble/curves): the twelve base-field coefficients in ascending tower order — Fp12 = c0 || c1 over Fp6, Fp6 = c0 || c1 || c2 over Fp2, Fp2 = c0 || c1 over Fp — each coefficient a 48-octet big-endian integer, 576 octets in all. This ties the transcript to a specific field encoding: a change to that encoding, or to the underlying pairing library's layout, changes every challenge and is caught by the golden vectors before anything downstream. Absorbing GT elements, rather than re-deriving them, keeps the Verifier's reconstruction Section 6.3.2 inside the challenge.

4.5. One challenge per transcript

A transcript yields exactly one challenge. Absorbing after the challenge has been drawn, or requesting a second challenge, MUST fail. This is not hygiene; it is the core soundness property. Sharing a Schnorr blinding for a message m across two proofs that use different challenges c_1 != c_2 hands the Verifier the message outright:

m^_1 - m^_2 = (m~ + c_1 * m) - (m~ + c_2 * m) = (c_1 - c_2) * m
=> m = (m^_1 - m^_2) / (c_1 - c_2)

Every credkit construction that shares a blinding — witness equality, range binding, set membership binding, and accumulator membership binding — relies on the two responses being taken under the same c so that this subtraction yields 0 and reveals nothing. The reference implementation demonstrates the recovery end-to-end as a test, to keep the "one challenge" rule from ever being relaxed.

5. Composite presentations

5.1. Message space and index conventions

Message indices are in message space, identical to the blind BBS interface: signer-known messages occupy 0 .. L-1 and holder-committed messages occupy L .. L+M-1. The proof-space mapping — which inserts the always-hidden secret_prover_blind at proof index L — stays inside the BBS blind setup and is never exposed upward. Consequently the prover-blind slot cannot be the target of an equality constraint or a predicate: it is unreachable by construction.

5.2. Structure

A Presentation produced by the Prover is:

  • proofs: one BBS proof per statement, each carrying the same challenge;
  • rangeProofs: one range proof per range predicate, in spec order;
  • membershipProofs: one set-membership proof per membership predicate, in spec order;
  • accumulatorProofs: one accumulator-membership proof per accumulator predicate, in spec order;
  • challenge: the single merged challenge.

The Verifier is given the public StatementDescriptor for each statement (public key, header, disclosed messages, issuer-known count), the same PresentationSpec (equality constraints and predicate lists) the Prover used, and the same presentation header. Accumulator predicates in that spec contain registry state obtained by the Verifier, not accepted from the Prover.

5.4. Randomness generation and independence

Every random scalar in every layer — the BBS Schnorr blindings, equality-class blindings, the predicate blinding factors v and v~, the accumulator randomizers rho and rho~, and the registry's own secrets — is drawn by the calculate_random_scalars procedure recommended by [I-D.irtf-cfrg-bbs-signatures]: OS2IP over expand_len (48) uniformly random octets, reduced mod r, keeping bias below 2^-128. The source is pluggable per operation so that fixtures can substitute the drafts' seeded_random_scalars mock, which is how every randomized operation in the reference implementation has a byte-reproducible golden vector.

Reusing one stateless randomness source across statements is the realistic way to accidentally share a blinding where independence is required (for example a shared e~, which would leak e_1 - e_2 = (e^_1 - e^_2)/c across two signatures). The Prover therefore enforces independence rather than assuming it: it rejects a presentation in which two statements drew identical randomness. Predicate randomness is subject to a stronger, joint check described in Section 9.4. Accumulator proof randomizers join the same independence pool as range and set-membership proof randomizers.

6. Predicates over hidden messages

Both predicate kinds are instances of the set-membership protocol of [CCS08], whose security rests on the u-Strong Diffie-Hellman assumption via Boneh-Boyen signatures [BB04]. Set membership is the base primitive; a range proof is its digit-decomposition composition.

6.1. Parameters: a signed alphabet

A predicate's public parameters are a Boneh-Boyen signed set. The Verifier picks a signing scalar x once, publishes y = G2.BASE * x and one signature per set element,

A_j = G1.BASE * (1 / (x + members[j]))    (mod r in the exponent)

and discards x. For a range predicate the set is the consecutive digit alphabet {0, ..., u-1} (base = u). For a set-membership predicate the set is any list of distinct scalars in publication order. This is the entire "trusted setup": a Verifier that signs values outside the intended set only enables proofs it alone would accept; that is, it fools only itself. A consumer importing third-party parameters can validate every signature by checking e(A_j, y + G2.BASE * members[j]) == e(G1.BASE, G2.BASE); this catches a malformed alphabet but, by design, cannot catch a Verifier that hands out per-prover alphabets as tracking tags (see Section 9.11).

The Boneh-Boyen bases are the curve's standard generators, unrelated to the BBS message generators. The only quantity ever shared between a predicate proof and a BBS proof is a response scalar, never a group element, so no cross-family independence assumption is needed.

6.2. Set membership

To prove a hidden value m (signed as a numeric BBS message) is one of the set members, the Prover selects the signature A on m, blinds it as V = A * v for a uniformly random nonzero v (so V is uniform in G1 \ {O}), and runs a Schnorr-style proof of the pairing relation

e(V, y) = e(V, G2.BASE)^(-m) * e(G1.BASE, G2.BASE)^(v)

which holds if and only if V is a blinded Boneh-Boyen signature on m. Concretely:

first move:   R = e( V * (-m~) + G1.BASE * v~ ,  G2.BASE )
responses:    response         = m~ + c * m
              blindingResponse = v~ + c * v

where m~ is supplied by the composite layer Section 6.3.2 and v~ is fresh. The Verifier reconstructs

R = e( V * (-response) + G1.BASE * blindingResponse ,  G2.BASE )
    * e( V * (-c) ,  y )

using one batched pair of pairings, absorbs V and R into the transcript at the corresponding Prover position, and confirms that the re-derived challenge matches.

The step that ties this to the credential is the binding: the composite layer passes the referenced BBS message's Schnorr blinding as m~ directly. Then response = m~ + c * m is, by construction, the same scalar as the BBS proof's response m^ for that slot — but only if the membership proof's m equals the credential's message and the challenge is shared. The Verifier checks response == m^. Under u-SDH a value outside the signed set would require a Boneh-Boyen forgery, so a passing proof establishes "the credential's hidden message is one of the signed members" without revealing which member it is.

6.3. Range via digit decomposition

A range predicate proves a hidden value lies in [0, u^L) by decomposing it into L base-u digits and running one membership proof per digit against the alphabet {0, ..., u-1}. Writing value = sum_i u^i * d_i with each d_i in the alphabet:

per digit i:  V_i = A_{d_i} * v_i,   v_i uniform nonzero
              R_i = e( V_i * (-d~_i) + G1.BASE * v~_i ,  G2.BASE )
              d^_i = d~_i + c * d_i
              v^_i = v~_i + c * v_i

The digits are bound to a single value by the aggregate response:

sum_i u^i * d^_i = ( sum_i u^i * d~_i ) + c * value

The right-hand blinding sum_i u^i * d~_i is the quantity the composite layer controls. The Prover draws the free blindings d~_0 .. d~_{L-2} at random and solves the last one, d~_{L-1}, so that sum_i u^i * d~_i equals a target it chooses; conditioned on the free draws this last value is uniform, so nothing leaks. The proof carries V_i, d^_i, v^_i per digit; there is no aggregate value on the wire — the Verifier recomputes sum_i u^i * d^_i from the digit responses.

6.3.1. One-sided ranges and the bound

credkit exposes range predicates as one-sided comparisons against an inclusive bound:

  • greaterOrEqual: prove (m - bound) mod r lies in [0, u^L). The composite layer targets the aggregate blinding to m~ (the message's own Schnorr blinding).
  • lessOrEqual: prove (bound - m) mod r lies in [0, u^L). The aggregate blinding is targeted to -m~.

With those targets, the aggregate response equals a shift of the BBS response, and the Verifier's binding check Section 6.3.2 is a single linear relation. A two-sided range is two predicates against the same slot. This one-sided form is what makes "older than N" a single predicate: encode the date so that the honest difference is small and positive, and the incidental upper bound u^L is a harmless "younger than ~179 years".

6.3.2. Binding a predicate to a signature

Let m^ be the BBS response for the referenced slot (the value the Verifier already reconstructs for that hidden message), c the merged challenge, and bound the predicate bound. After confirming the predicate's own sigma commitments re-derive the shared challenge, the Verifier checks, with sigma = sum_i u^i * d^_i:

greaterOrEqual:  sigma == m^ - c * bound       (mod r)
lessOrEqual:     sigma == c * bound - m^        (mod r)
membership:      response == m^                 (mod r)

These are the checks that bind the predicate to the signed value rather than leaving it unbound to the credential. The reference implementation's tests include a Prover that runs perfectly valid digit or membership proofs over the wrong value; every such proof is caught here and only here, which is why the predicate package deliberately provides no self-contained "verify" and no internal challenge. A rewinding extractor recovers sum_i u^i * d_i = value (range) or the member (set) with each digit alphabet-bound, establishing the range or membership; the shared-blinding equality against m^ establishes that value is the credential's hidden message.

7. Accumulator non-revocation

credkit models a revocation registry as the current set of unrevoked credential identifiers and proves positive membership in that set. It uses the positive VB accumulator [VB20] over BLS12-381, operated with additions held static as in the KB construction [KB21]. A revocation identifier y is a fresh uniform scalar assigned by the Issuer and signed into the credential as a hidden numeric message.

This design deliberately does not use a bitstring status list: disclosing a stable per-credential index would undo BBS verifier unlinkability. It also does not implement the VB universal accumulator. Non-membership witnesses are unnecessary for an unrevoked-set registry and create the trapdoor-recovery surface analyzed in [VB-ATTACK].

7.1. Registry setup and issuance with static additions

The revocation authority samples and retains a nonzero alpha and publishes:

Qtilde = alpha * G2.BASE
params = { Qtilde }

It initializes the accumulator as V_0 = u_0 * G1.BASE for a fresh nonzero u_0, then discards u_0. To enroll a credential with hidden revocation identifier y, it issues the membership witness:

C = (1 / (y + alpha)) * V

The negligible-probability case y = -alpha has no inverse and MUST be rejected; the Issuer resamples the identifier.

The value V does not change when credentials are issued. There is no public addition record and no other holder updates a witness. This makes joins invisible in the public registry history and avoids the public batch-addition data attacked in [ALLOSAUR]. Implementations conforming to this construction MUST NOT publish or consume addition update data.

7.2. Deletion-only epochs and holder updates

To revoke a non-empty batch D = [y_0, ..., y_{m-1}], the authority publishes a sequential epoch record containing the new accumulator V', the removed identifiers, and one G1 point per coefficient of the VB witness-update polynomial:

V' = (1 / product_{y_i in D}(y_i + alpha)) * V
v_D(X) = sum_{s=1}^{m} (
           (1 / product_{i=1}^{s}(y_i + alpha)) * product_{j=1}^{s-1}(y_j - X)
         )
       = sum_{i=0}^{m-1} c_i * X^i
Omega_i = -c_i * V                 for i in 0..m-1
update = (epoch, V', D, [Omega_0, ..., Omega_{m-1}])

The raw coefficients, which depend on the trapdoor, MUST NOT be published. The Ω points and removed identifiers are identical for every holder and can be distributed as static public data. A holder applies one or more strictly increasing updates locally with one combined multiscalar multiplication and one field inversion. The work is linear in the number of removals, matching the non-interactive lower bound of [CH09]. If an update removes the holder's own y, then d_D(y) = product_{y_i in D}(y_i - y) = 0; witness update MUST fail. Otherwise one epoch updates the witness as C' = (C + sum_i y^i * Omega_i) / d_D(y). No witness for the new V' can be produced when the denominator is zero; that failure is the revocation operation.

The witness is mutable wallet-side state. It MUST NOT be signed into the credential or serialized in the presentation envelope. A holder SHOULD verify a witness against an untrusted update feed before relying on it:

e(C, Qtilde + y * G2.BASE) == e(V, G2.BASE)

7.3. CDH membership proof and BBS binding

The Prover proves that its hidden signed y has a valid witness for the Verifier-accepted V using the CDH weak-BB proof. For fresh nonzero rho and fresh rho~, with y~ set to the BBS slot's Schnorr blinding:

CPrime = rho * C
CBar   = rho * V - y * CPrime       # equals alpha * CPrime for a valid witness
T      = rho~ * V - y~ * CPrime
s_rho  = rho~ + c * rho

The wire proof is (CPrime, CBar, s_rho). It intentionally omits an element response. The Verifier takes s_y = y~ + c * y from the referenced hidden BBS message and checks:

CPrime != identity
CBar   != identity
e(CBar, G2.BASE) == e(CPrime, Qtilde)
T == s_rho * V - s_y * CPrime - c * CBar

It then absorbs CPrime, CBar, and reconstructed T at the transcript position shown in Section 4.2 and re-derives c. The shared response makes the accumulator proof about the same y that the Issuer signed. Supplying any other response proves membership of nothing and fails the merged challenge. An accumulator proof is therefore a revocation gate on a BBS credential, never a standalone authenticator.

7.4. Verifier-stated freshness

An accumulator predicate names the credential statement and hidden message index, registry public parameters, accumulator value, and epoch. The Verifier MUST obtain the parameters, value, and epoch through its own trusted registry channel and pass them into verification. These fields are absorbed into the transcript. A Prover cannot select a stale or fabricated registry state by carrying it in the proof.

The maximum tolerated age of a registry epoch is application policy, like status-list caching. Accepting a window is permitted, but presenting against an older accepted epoch can reveal the holder's last synchronization time. Holders SHOULD update to the newest available accepted epoch before presenting.

8. Wire formats

All multi-octet lengths are I2OSP(_, 8). pointLength = 48 (G1) and scalarLength = 32; a G2 encoding is 96 octets.

8.1. Range parameters

range_params := I2OSP(base, 8) || publicKey || A_0 || ... || A_{base-1}

publicKey is the 96-octet G2 encoding of y; each A_i is 48 octets. Deserialization validates every point and rejects the identity.

8.2. Set-membership parameters

set_params := I2OSP(count, 8) || publicKey || ( I2OSP(members[j], 32) || A_j )  for j in 0..count-1

Members are distinct scalars in [0, r), serialized in publication order; the order is part of the transcript.

8.3. Range proof

range_proof := I2OSP(L, 8)
            || V_0 || ... || V_{L-1}            # L * 48 octets
            || d^_0 || ... || d^_{L-1}          # L * 32 octets
            || v^_0 || ... || v^_{L-1}          # L * 32 octets

Total 8 + L * 112 octets. Deserialization rejects an identity V_i and any out-of-range scalar.

8.4. Set-membership proof

membership_proof := V || response || blindingResponse

Fixed 112 octets (48 + 32 + 32). Deserialization rejects an identity V.

8.5. Accumulator parameters

accumulator_params := Qtilde

Qtilde is the 96-octet G2 encoding of the registry public key. Deserialization validates the point and rejects the identity.

8.6. Registry update

registry_update := I2OSP(epoch, 8) || value || I2OSP(m, 8)
                || y_0 || ... || y_{m-1}
                || Omega_0 || ... || Omega_{m-1}

value and each Omega_i are 48-octet G1 encodings; each removed y_i is a 32-octet scalar. m MUST be positive. The accumulator value MUST NOT be the identity. An Ω entry MAY be the identity because a zero polynomial coefficient is valid.

8.7. Accumulator-membership proof

accumulator_proof := CPrime || CBar || s_rho

Fixed 128 octets (48 + 48 + 32). Deserialization rejects identity points and out-of-range scalars. The element response s_y is intentionally absent; verification obtains it from the referenced BBS proof.

8.8. Presentation

The presentation carries the shared challenge exactly once, at the end, so a per-proof challenge mismatch is unrepresentable on the wire. Each BBS proof is serialized without its trailing challenge scalar.

presentation := I2OSP(N, 8)
     || per statement:   I2OSP(len, 8) || bbs_proof_without_challenge
     || I2OSP(K, 8)
     || per range pred.: I2OSP(len, 8) || range_proof
     || I2OSP(M, 8)
     || per membership:  I2OSP(len, 8) || membership_proof
     || I2OSP(A, 8)
     || per accumulator: I2OSP(len, 8) || accumulator_proof
     || challenge        # 32 octets

During parsing, the challenge is reattached to each BBS-proof body and the fully validating BBS, range, membership, and accumulator parsers are reused; the recovered challenge is taken from the first BBS proof, and verification Section 5.2 independently re-derives it and compares.

9. Security considerations

9.1. Soundness rests on one challenge

As shown in Section 4.5, all shared-blinding constructions become unsound — in fact witness-revealing — if composed under more than one challenge. An implementation MUST derive one challenge over one transcript for the whole presentation. Composing linked, predicate, or non-revocation proofs from independent single-proof invocations is the specific error to avoid.

9.2. u^L <= 2^64 is a soundness bound, not an implementation limit

The one-sided range trick relies on a negative difference (m - bound) mod r being a ~2^255 scalar that cannot be written as sum_i u^i * d_i with each d_i < u and u^L <= 2^64. That undecomposability only holds while u^L is far below r. A Verifier that accepted L large enough for u^L to approach r would have proven nothing — every scalar would be "in range". Both the Prover and the Verifier MUST reject u^L > 2^64.

9.3. Identity V must be rejected

An identity V satisfies the digit/membership pairing relation for any claimed digit or member with v = 0, which voids the alphabet bound entirely. Provers draw v != 0; Verifiers and deserializers MUST reject an identity V.

9.4. Shared alphabet randomness is brute-forceable

Independence of the blinding v across alphabet proofs is not merely hygiene here. If two alphabet proofs reuse one v, then V_1 = A_a * v and V_2 = B_b * v satisfy

e(V_1, y_1 + a * G2.BASE) == e(V_2, y_2 + b * G2.BASE)

and because alphabets are small (a base-16 digit alphabet, a handful of set members), a Verifier can sweep all candidate pairs (a, b) and recover both hidden values outright — the values, not just a relation, at trivial cost. The Prover therefore enforces first-drawn-scalar independence jointly across all range and membership proofs in a presentation, not per proof. The reference implementation places accumulator-proof randomizers in the same collision guard so that a misconfigured deterministic randomness source is rejected across every predicate kind.

9.5. Accumulator proofs require a credential binding

The CDH membership proof in Section 7.3 is not a standalone proof of credential validity. Its omitted s_y is a deliberate partial-proof interface: the Verifier MUST source that scalar from the referenced hidden BBS slot under the same merged challenge. Accepting a caller-supplied element response, or treating accumulator membership alone as authentication, would sever the proof from the issuer signature.

The Verifier MUST reject identity CPrime, identity CBar, an identity accumulator value, a failed pairing relation, and a missing or disclosed referenced slot.

9.6. Positive-only, deletion-only registry

An implementation MUST NOT issue non-membership witnesses. The universal VB variant needs an additional secret initialization to address the pooled-witness trapdoor-recovery attack of [VB-ATTACK]; credkit has no use for that variant because revocation is removal from an unrevoked set.

An implementation also MUST NOT publish accumulator addition updates. The witness formula for static additions permits issuance without changing public state, while public VB batch-addition data enables the attack described by [ALLOSAUR]. Deletion-only Ω data does not provide the same forgery path. Binding every membership proof to an issuer-signed hidden identifier further makes a forged witness insufficient by itself.

9.7. Registry freshness is verifier policy

The merged transcript binds the registry public key, accumulator value, and epoch supplied by the Verifier. Cryptographic verification establishes non-revocation as of exactly that state; it does not decide whether the state is fresh enough. Verifiers MUST obtain registry state independently of the presentation and SHOULD set an explicit maximum tolerated age. A carried wire copy may be used for diagnostics but MUST NOT replace verifier-supplied state.

9.8. Replay and the presentation header

A presentation is publicly verifiable, so anyone who obtains one can replay it to any Verifier that would accept the same transcript. What prevents that is not the proof system but the presentation header: the application places a Verifier-supplied nonce (and, where useful, an audience identifier) there, and the transcript binds it into the one challenge, so a transcript answering one Verifier's nonce cannot answer another's. Whether a nonce is fresh is Verifier policy, exactly like registry freshness. The cryptosuite layer of Section 12 folds the W3C proof options challenge and domain into this header as length-framed fields under a dedicated DST — domain encoded even when empty, one layout — so a VP's authentication options are covered by the challenge with no change to the composite layer.

9.9. Modular predicate semantics

A predicate proves a statement about scalars mod r: greaterOrEqual proves (m - bound) mod r ∈ [0, u^L). It reads as the natural >=/<= only because the application encodes honest values well below r — for example a date of birth as days since 1900, or an integer under 2^64. An application that signs values near r and expects signed comparison is using the predicate outside the regime where its plain-language reading holds. This is an application responsibility; the library enforces the 2^64 ceiling on u^L but cannot know an application's intended encoding.

9.10. Trusted setup and u-SDH

The only trust the parameters require is that the signing scalar x was discarded. A Verifier that keeps x gains nothing it did not already have — it can already accept or reject at will; the alphabet is its own acceptance policy. Soundness against the Prover reduces to u-SDH: exhibiting a valid blinded signature on a value outside the signed set is a Boneh-Boyen forgery. A consumer using third-party parameters SHOULD validate them once on import.

9.11. Per-prover alphabets are a linkability trap

Parameter validation confirms an alphabet is well-formed; it cannot detect a Verifier that issues a distinct well-formed alphabet to each Prover, turning an otherwise unlinkable predicate proof into a per-Prover tag. This is the same linkability class as per-Prover BBS generators. Provers SHOULD fetch predicate parameters from the same published location as every other Prover, and treat a Verifier-specific alphabet as a red flag.

9.12. Registry equivocation is the same trap

The accumulator's update feed has the same failure mode as a per-prover alphabet. Deletion epochs are meant to be one public sequence common to all holders; an operator that serves a targeted Holder a forked feed — a different accumulator value under the same epoch number — partitions its holders. The targeted Holder's witness then verifies only against the forked value, so it either fails against a Verifier's canonical state or, where a Verifier tries more than one accepted value, identifies itself by which one its proof binds. Witness verification (Section 7.2) detects a malformed feed, not a forked one. The defense is the alphabet defense: Holders SHOULD obtain registry updates from the same published location as every other holder and treat conflicting values for one epoch as a red flag.

9.13. Transcript order binding

Statement, constraint, and predicate order is bound into the challenge and is not canonicalized. A Prover and Verifier that disagree on order produce different challenges and the presentation fails closed, rather than a canonicalizer masking the disagreement. Likewise the full serialized predicate parameters — including the member list and its order — are absorbed, so a Verifier cannot narrow, widen, re-sign, or reorder the set after the fact without the challenge changing.

9.14. Fail-closed verification

Verification returns a boolean and MUST return false on any malformed input, broken transcript, or failed check; the octet parsers throw on malformed input. Every point and scalar entering from the wire is range- and subgroup-validated.

10. Privacy considerations

10.1. Verifier unlinkability is preserved by omission

BBS proofs are randomized per presentation, so unlinkability is a property credkit must avoid destroying, not one it must add. It is preserved by never placing a stable, disclosed value in a presentation: predicates disclose the truth of a comparison or accumulator membership, never a commitment, status index, or value. This is the specific improvement over disclosing a fixed commitment or stable status-list index, either of which is a perfect cross-verifier correlation handle.

10.2. What a predicate reveals

A range predicate reveals only that the hidden value satisfies the one-sided comparison (and, as a side effect of the construction, that it lies below u^L). A membership predicate reveals only that the hidden value is one of the published members, never which. An equality constraint reveals only that the referenced slots are equal, never the shared value. The number and kind of predicates, and the parameters used, are of course visible in the spec both parties share.

An accumulator predicate reveals that the credential's hidden revocation identifier is a member of one specific public accumulator value at one epoch. It does not reveal the identifier or the membership witness. The registry public key, accumulator value, and epoch are public and common to all holders using that registry state.

10.3. Revocation update privacy

Issuance produces no public registry event. Each deletion epoch publishes the same removed identifiers and Ω data to every holder, so witness synchronization needs no per-holder query and creates no holder-specific correlation channel. The update feed does reveal the random scalars removed in an epoch, but those values are never disclosed from credentials and are not meaningful outside the registry.

A Verifier that accepts multiple epochs learns which accepted state a holder used. A stale epoch can therefore fingerprint the holder's synchronization lag. Holders SHOULD update before presenting, and Verifiers SHOULD keep any acceptance window no wider than operationally required.

11. Known limitations and open issues

An honest design record must describe what remains unproven, unhardened, or structurally weak, not only what works. Each limitation below is a known consequence of a design choice, recorded so that readers can weigh it directly.

11.1. The composition has no formal security proof

Every primitive credkit uses — BBS, blind BBS, the CCS set-membership protocol, Boneh-Boyen signatures, the VB accumulator — has published analysis. The composition does not. The claim that sharing Schnorr blindings across BBS, CCS, and accumulator statements under one merged challenge yields a sound AND-composition with knowledge extraction is argued informally in this document (Section 4.5, Section 6.3.2) and exercised by adversarial tests, but no reduction has been written down, no machine-checked or peer-reviewed proof exists, and the construction has had no external cryptographic review. AND-composition of sigma protocols is standard; this particular assembly — partial-proof response sharing across three proof families, one of them pairing-based with GT commitments — is bespoke. A concrete instance of the gap is the accumulator operation with static additions adopted from [KB21]. It carries a soundness argument in a non-adaptive model. credkit's compensations — revocation identifiers are issuer-assigned uniform scalars, never holder-chosen, and a forged witness binds to nothing without a BBS signature over the same identifier — are architectural, not a proof in the stronger model. Treat soundness as plausible and tested, not established.

11.2. Holder binding is knowledge of a scalar

Credentials here are not bearer instruments. Every presentation proves knowledge of all hidden messages, including the blind-signed link secret — a value the Issuer never saw and no transcript contains. A stolen signature alone is therefore insufficient to create a presentation.

The binding is knowledge-based, not device-based. Anyone who obtains the full witness — signatures, messages, the link secret, and membership witnesses — has everything needed to present. Exfiltrating that state enables complete impersonation across every credential bound to the shared link secret.

Nothing cryptographic prevents a cooperative Holder from lending the witness. Knowledge, unlike a non-extractable hardware key, can be copied freely. Credential pooling is deterred by the all-or-nothing cost of sharing one's entire credential persona, not prevented by the construction.

The absence of a hardware-bound presentation key is a choice with a reason: a device key that signed each presentation would be a correlation handle unless its signature were itself proven in zero knowledge, and proving a secure-element signature inside a proof is the SNARK-scale machinery this construction exists to avoid. A deployment can anchor the scalar in hardware — the motivating wallet derives it from a passkey PRF output, so stored state alone is not the secret — but the protocol neither requires nor attests that, and revocation (Section 7) remains the only after-the-fact remedy.

11.3. Adaptive predicate bounds can binary-search a hidden value

A range predicate reveals one bit. A Verifier free to choose bounds adaptively across sessions can spend those bits on a binary search. After roughly log2(|D|) presentations, where D is the value domain, the Verifier can recover the hidden value exactly. A Holder's refusal to answer an unusual bound can also leak a bit. The construction cannot prevent this — each individual proof does exactly what it claims. The mitigation is policy: bounds SHOULD be fixed, published policy constants (a statutory age, a regulatory threshold) common to every Holder. A Verifier that varies bounds per Holder or per session is the range-predicate analog of the per-Prover alphabet of Section 9.11.

11.4. Presentations are transferable evidence

A presentation is non-interactive and publicly verifiable, and that cuts both ways: anyone who later obtains the transcript and the Verifier's inputs can re-run verification and be exactly as convinced as the Verifier was. What a Holder discloses is therefore not merely data but issuer-endorsed evidence — a Verifier's breach or onward sharing leaks provably authentic claims, not deniable assertions, and the Holder cannot repudiate them afterward. Restoring deniability would take a designated-verifier or interactive variant of the proof, which credkit does not provide. Verifiers holding presentations SHOULD treat them with the sensitivity of certified documents, not form submissions.

11.5. Key compromise is silent

Every trust anchor here fails undetectably, because the proofs its compromise enables are zero-knowledge and therefore indistinguishable from honest ones. A compromised Issuer key forges credentials, as in any signature scheme. A compromised alphabet scalar x — one that was not discarded as Section 9.10 requires — lets a Prover prove any value against that alphabet. A compromised registry alpha lets anyone compute a valid witness for any identifier, silently disabling revocation for the whole registry (though not authentication: the binding of Section 9.5 still demands a BBS-signed identifier). None of these has a detection mechanism or a recovery story beyond rotation and reissuance. Separately, u-SDH security degrades as signed sets grow — a Cheon-style loss of up to roughly log2(q)/2 bits for a q-element set — negligible for digit alphabets, but a reason to keep set-membership alphabets small.

11.6. The implementation is not hardened against side channels

The reference implementation is pure TypeScript over @noble/curves. JavaScript BigInt arithmetic is not constant-time, secret scalars — the link secret above all — pass through garbage-collected memory that cannot be reliably zeroized, and no masking beyond what the protocols themselves require is applied. The implementation optimizes for auditability of the cryptographic logic, not resistance to a co-resident timing adversary; a threat model that includes one needs a hardened implementation.

11.7. Post-quantum: soundness fails, privacy is unassessed

All soundness rests on discrete-log-family assumptions in pairing groups (u-SDH, CDH, and the BBS reductions). A cryptographically relevant quantum computer would invalidate every soundness guarantee. Hiding is on better footing — sigma responses are statistically blinded and the blinded signature values are uniform group elements — but no everlasting-privacy analysis of the full composite has been done. The privacy of recorded transcripts against a future quantum adversary should therefore be treated as unevaluated rather than guaranteed.

11.8. Unlinkability ends at the cryptography

The unlinkability of Section 10 covers the transcript and nothing around it. The combination of disclosed values can identify a Holder outright; an unusual request shape — which claims, which predicates, which bounds — creates a fingerprint. Network metadata and timing can correlate visits even though the proofs cannot. At the cryptosuite layer, the structure of the disclosed document (quad and blank-node counts) reveals the credential's schema, a property of the canonicalization that bbs-2023 itself notes. The Issuer's public key is visible in every presentation, so a Holder's anonymity set is never larger than that Issuer's holder population — a credential from a ten-holder registry identifies its holder nearly as well as a name would. The epoch-lag channel of Section 10.3 is one instance of the general rule: the cryptography removes protocol-level correlation handles, and everything else is application discipline.

12. The cryptosuite layer

A JSON-LD Data Integrity cryptosuite that packages these presentations as W3C Verifiable Credentials [VC-DATA-MODEL], in the manner of bbs-2023 [DI-BBS], is implemented for both the single-credential and multi-credential cases. It is summarized here to place the layers above in context; its full specification is a separate matter and is not attempted in this document.

12.1. Relationship to bbs-2023

The cryptosuite keeps RDF Dataset Canonicalization of the credential, the per-base-proof HMAC label shuffle, and the JSON-Pointer split between mandatory and selectively disclosed statements. Mandatory statements are folded into the BBS header as proofHash || mandatoryHash. It also keeps one BBS message per non-mandatory N-Quad and a CBOR-then-multibase proof-value envelope.

The proof itself is replaced. Every derived proof is a credkit presentation Section 5 rather than a single BBS proof. This includes plain selective disclosure with no predicates, so there is one derivation path and no bbs-2023-shaped special case. It applies the uniform-single-statement rule of Section 3.4 one layer up.

The cryptosuite identifier, @context, and CBOR envelope prefix bytes are disjoint from those of bbs-2023. A credkit proof value therefore cannot parse as a bbs-2023 proof value, and the suite makes no interoperability claim.

12.2. Numeric twin messages

A predicate needs the message scalar to be the value Section 3.1. A bbs-2023 message, however, is a hash of an N-Quad string, so no arithmetic survives the hash. The Issuer therefore appends one numeric ("twin") message after the quad messages for each entry in a declared list of (JSON-Pointer, encoderId) pairs. The value at that pointer is encoded as an integer message; for example, a date can be encoded as the number of days since 1900.

The twin block is always hidden by construction. It is bound as a third BBS header segment:

bbsHeader = proofHash || mandatoryHash || H(serialize(numericDecl))

The signature itself provides the binding, so a Prover cannot misrepresent a slot's meaning without breaking header reconstruction. The declaration adds no per-credential correlation handle to the disclosed document.

Range, set-membership, equality, and non-revocation claims point at twins, subject to each encoder's allowed uses. Equality yields a cross-issuer equality proof over a hidden value when both issuers declare the same predicate-safe encoder.

12.3. Revocation policy and sidecars

The Issuer creates a fresh uniform Fr scalar and places its canonical decimal form at a declared /credentialStatus/revocationId pointer using the frScalar encoder. The twin is signed and always hidden. Unlike predicate-safe values such as dates and unsigned integers, an frScalar twin cannot be targeted by range, set-membership, or equality claims. Those operations would let a Verifier probe or correlate a permanent identifier. Conversely, a non-revocation claim MUST target an frScalar twin. The source N-Quad for the identifier is non-disclosable even through a subtree selection such as /credentialStatus.

A current membership witness enters the Prover's presentGraph call but is never serialized. The Verifier's expectedNonRevocationClaims supplies registry parameters, accumulator value, and epoch from an independent fetch. The VP envelope carries [statement, declaration index, params hash, accumulator, epoch] only as cross-checks that enable useful synchronization errors. Verification uses the Verifier's values in the transcript, never the carried copies.

12.4. Multi-credential presentations

The multi-credential case — N credentials under one merged challenge inside a Verifiable Presentation — is secured by a second Data Integrity cryptosuite. Each embedded credential carries a statement descriptor: its reconstruction mode, label map, index sets, N-Quads, and numeric declaration. The Issuer's verificationMethod is carried as an identifier in the sibling field of the credential's proof, never as a key.

The presentation-level proof carries the merged credkit presentation of Section 5, together with the equality constraints and per-statement claim lists, including any non-revocation gates. Under the @container: @graph semantics of the VC v2 context, the presentation body is only a carrier and need not be hashed by the presentation proof. Each credential is already bound by its own BBS signature and absorbed into the merged transcript. Reordering or substituting credentials that differ in disclosed content therefore breaks the challenge without an envelope-level integrity check.

One ciphersuite is pinned across the whole presentation because the link-secret witness is suite-dependent. A holder identifier is unrepresentable by construction. The repository design record and its rationale are in FINDINGS.md §§14–19. Those sections describe the cryptosuite envelope and application vocabulary; this document specifies the presentation, predicate, and accumulator layers on which they rely.

13. IANA considerations

This document has no IANA actions. It defines no new registries and requests no code points.

14. References

14.1. Normative References

[I-D.irtf-cfrg-bbs-blind-signatures]
Kalos, V. and G. M. Bernstein, "Blind BBS Signatures", Work in Progress, Internet-Draft, draft-irtf-cfrg-bbs-blind-signatures-03, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bbs-blind-signatures-03>.
[I-D.irtf-cfrg-bbs-signatures]
Looker, T., Kalos, V., Whitehead, A., and M. Lodder, "The BBS Signature Scheme", Work in Progress, Internet-Draft, draft-irtf-cfrg-bbs-signatures-10, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bbs-signatures-10>.

14.2. Informative References

[ALLOSAUR]
Jaques, S., Lodder, M., and H. Montgomery, "ALLOSAUR: Accumulator with Low-Latency Oblivious Sublinear Anonymous credential Updates with Revocations", IACR Cryptology ePrint Archive 2022/1362, , <https://eprint.iacr.org/2022/1362>.
[ANONCREDS-V2]
AnonCreds Project, "AnonCreds v2 Rust implementation (crate credx)", , <https://github.com/anoncreds/anoncreds-v2-rs>.
[BB04]
Boneh, D. and X. Boyen, "Short Signatures Without Random Oracles", In EUROCRYPT, , <https://link.springer.com/chapter/10.1007/978-3-540-24676-3_4>.
[CCS08]
Camenisch, J., Chaabouni, R., and a. shelat, "Efficient Protocols for Set Membership and Range Proofs", In ASIACRYPT, , <https://link.springer.com/chapter/10.1007/978-3-540-89255-7_15>.
[CH09]
Camacho, P. and A. Hevia, "On the Impossibility of Batch Update for Cryptographic Accumulators", IACR Cryptology ePrint Archive 2009/612, , <https://eprint.iacr.org/2009/612>.
[DI-BBS]
W3C, "Data Integrity BBS Cryptosuites v1.0", , <https://www.w3.org/TR/vc-di-bbs/>.
[FrozenHeart]
Trail of Bits, "The Frozen Heart vulnerability in Fiat-Shamir implementations", , <https://blog.trailofbits.com/2022/04/13/part-1-coordinated-disclosure-of-vulnerabilities-affecting-girault-bulletproofs-and-plonk/>.
[I-D.irtf-cfrg-fiat-shamir]
Orrù, M., "Fiat-Shamir Transformation", Work in Progress, Internet-Draft, draft-irtf-cfrg-fiat-shamir-02, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-fiat-shamir-02>.
[I-D.irtf-cfrg-sigma-protocols]
Orrù, M. and C. Yun, "Interactive Sigma Proofs", Work in Progress, Internet-Draft, draft-irtf-cfrg-sigma-protocols-02, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-sigma-protocols-02>.
[KB21]
Karantaidou, I. and F. Baldimtsi, "Efficient Constructions of Pairing Based Accumulators", IACR Cryptology ePrint Archive 2021/638, , <https://eprint.iacr.org/2021/638>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC8017]
Moriarty, K., Ed., Kaliski, B., Jonsson, J., and A. Rusch, "PKCS #1: RSA Cryptography Specifications Version 2.2", RFC 8017, DOI 10.17487/RFC8017, , <https://www.rfc-editor.org/info/rfc8017>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC9380]
Faz-Hernandez, A., Scott, S., Sullivan, N., Wahby, R. S., and C. A. Wood, "Hashing to Elliptic Curves", RFC 9380, DOI 10.17487/RFC9380, , <https://www.rfc-editor.org/info/rfc9380>.
[VB-ATTACK]
Biryukov, A., Udovenko, A., and G. Vitto, "Cryptanalysis of Au et al. Dynamic Universal Accumulator", IACR Cryptology ePrint Archive 2020/598, , <https://eprint.iacr.org/2020/598>.
[VB20]
Vitto, G. and A. Biryukov, "Dynamic Universal Accumulator with Batch Update over Bilinear Groups", IACR Cryptology ePrint Archive 2020/777, , <https://eprint.iacr.org/2020/777>.
[VC-DATA-MODEL]
W3C, "Verifiable Credentials Data Model v2.0", , <https://www.w3.org/TR/vc-data-model-2.0/>.

Author's Address

Tomislav Markovski
credkit