| Internet-Draft | credkit Composite Proofs | July 2026 |
| Markovski | Expires 20 January 2027 | [Page] |
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.¶
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.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
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 composite presentation layer that proves N BBS proofs under one merged Fiat-Shamir challenge, so that Schnorr blindings can be shared across proofs. Sharing a blinding for the same hidden message across two credentials, under one challenge, proves the messages are equal without revealing them — the "link secret" mechanic.¶
A predicate layer using the set-membership and range construction of Camenisch, Chaabouni, and shelat [CCS08]. A predicate proof is bound to a hidden BBS message not through a disclosed commitment but through a linear relation between response scalars that holds only under the shared challenge. The value stays hidden; only the predicate's truth is revealed.¶
An accumulator revocation layer using a positive VB bilinear accumulator [VB20], operated with additions held static and deletion-only public updates. Its CDH membership proof shares the hidden revocation identifier's BBS response, so the Verifier learns that the signed credential remains in the accepted registry state without learning the identifier.¶
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.¶
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:¶
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.¶
credkit is deliberately stratified so that each layer's relationship to prior work is clean:¶
| 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:¶
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].¶
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.¶
[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.¶
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):¶
| 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.¶
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].¶
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).¶
{0, ..., u-1}; for set
membership it is an arbitrary set of distinct scalars.¶
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.¶
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].¶
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.¶
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.¶
credkit exists because five privacy properties usually treated as separate features can, with BBS plus blind issuance, be realized in a single construction:¶
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).¶
+-------------------------------------------------------------+ | 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.¶
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.¶
[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).¶
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~;¶
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.¶
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.¶
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.¶
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).¶
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:¶
I2OSP(n, 8);¶
I2OSP(s, 32);¶
G1 point as its 48-octet compressed encoding;¶
GT element as its canonical serialization (see Section 4.4);¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
An equality constraint names two or more hidden message slots (across any statements) that must
contain the same value. The Prover draws one fresh Schnorr blinding per equality class and injects
it into every referenced slot's m~ before ProofInit fixes the first-move commitments. Under
the one merged challenge, each referenced slot's response is m^ = m~ + c * m with a common
m~; the Verifier checks that the response scalars at all referenced slots are equal. Equal
witnesses under a shared blinding and one challenge yield equal responses; unequal witnesses
cannot (that would require c * m_1 = c * m_2 for m_1 != m_2).¶
The Prover additionally checks that the witnesses are equal before proving (it holds them), and rejects a slot referenced by more than one class. The Verifier's check is purely on the response scalars and reveals nothing about the shared value.¶
The intended use is a link secret: a scalar the holder commits once (Commit) and has blindly
signed (BlindSign) into credentials from multiple issuers. A presentation with an equality
constraint on the link-secret slot of two statements proves the credentials belong to the same
holder, unlinkably between presentations and without the issuers ever learning the secret. The
blindness is essential — an issuer that learned the secret could, colluding with another issuer,
join all of a holder's credentials.¶
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.¶
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].¶
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.¶
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)¶
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.¶
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.¶
All multi-octet lengths are I2OSP(_, 8). pointLength = 48 (G1) and scalarLength = 32; a
G2 encoding is 96 octets.¶
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.¶
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.¶
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.¶
membership_proof := V || response || blindingResponse¶
Fixed 112 octets (48 + 32 + 32). Deserialization rejects an identity V.¶
accumulator_params := Qtilde¶
Qtilde is the 96-octet G2 encoding of the registry public key. Deserialization validates the
point and rejects the identity.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
This document has no IANA actions. It defines no new registries and requests no code points.¶