diff --git a/.gitignore b/.gitignore
index 5fe490f..5b0ca67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,9 @@ incubator/zeroj-prover-rapidsnark/src/main/resources/native/*/librapidsnark.*
### Rust build artifacts (Halo2) ###
incubator/zeroj-verifier-halo2/halo2-rust/target/
+zeroj-bbs/rust/target/
+zeroj-bls12381-wasm/rust/target/
+zeroj-bbs-wasm/rust/target/
### Go compiled binaries ###
zeroj-prover-gnark/gnark-wrapper/gentestvectors
diff --git a/docs/adr/0018-shared-bls12381-primitives-and-wasm-provider.md b/docs/adr/0018-shared-bls12381-primitives-and-wasm-provider.md
new file mode 100644
index 0000000..7706dd6
--- /dev/null
+++ b/docs/adr/0018-shared-bls12381-primitives-and-wasm-provider.md
@@ -0,0 +1,264 @@
+# ADR-0018: Shared BLS12-381 Primitives and Optional WASM Provider
+
+## Status
+Accepted
+
+## Date
+2026-05-07
+
+## Context
+
+ZeroJ already contains BLS12-381 functionality, but it is not exposed as one
+neutral primitive module:
+
+- `zeroj-crypto` contains optimized pure Java Montgomery field arithmetic and
+ Jacobian G1/G2 point arithmetic for BLS12-381 provers.
+- `zeroj-verifier-groth16` contains pure Java BLS12-381 affine field towers,
+ G1/G2 point types, and optimal Ate pairing checks under Groth16 verifier
+ packages.
+- `zeroj-blst` contains an optional native-backed wrapper for BLS12-381 pairing
+ and G1 operations through `foundation.icon:blst-java`.
+
+This split works for Groth16 and PlonK, but it is awkward for BBS. CFRG BBS
+needs reusable BLS12-381 primitives beyond a verifier-specific pairing check:
+compressed point codecs, subgroup checks, scalar arithmetic, hash-to-curve,
+message-to-scalar hashing, generator derivation, and pairing product checks.
+
+ADR-0017 introduced BBS support through Rust WASM, but later review showed that
+ZeroJ's existing pure Java BLS12-381 code makes a Java-native CFRG BBS
+implementation feasible. At the same time, a small compatibility spike showed
+that popular pure Rust BLS12-381 crates such as `zkcrypto/bls12_381` and
+`ark-bls12-381` can compile to `wasm32-unknown-unknown` and run under Chicory
+1.7.5 with no host imports. Native `blst` remains attractive for speed, but its
+Rust crate introduces C/assembly build complexity for `wasm32-unknown-unknown`.
+
+ZeroJ therefore needs a shared BLS12-381 primitive boundary before returning to
+the CFRG BBS implementation.
+
+## Decision
+
+### 1. Add `zeroj-bls12381`
+
+Create a neutral pure Java module named:
+
+```
+zeroj-bls12381
+```
+
+This module is the default BLS12-381 primitive provider for ZeroJ. It should
+expose reusable primitives needed by Groth16, PlonK, BBS, KZG, and future
+BLS12-381 protocols.
+
+The target API surface includes:
+
+- base field and scalar field helpers for `Fp`, `Fp2`, `Fp6`, `Fp12`, and `Fr`
+- G1 and G2 point operations: add, negate, scalar multiplication, identity,
+ equality, and subgroup validation
+- pairing operations: Miller loop, final exponentiation, and pairing product
+ check
+- compressed and uncompressed point serialization compatible with the relevant
+ standards
+- RFC9380 hash-to-curve helpers for BLS12-381 G1/G2
+- scalar hashing and canonical byte helpers needed by higher-level protocols
+
+This module must reuse or move existing ZeroJ pure Java implementations. It is
+not a second independent pure Java BLS12-381 implementation.
+
+### 2. Prefer a clean pre-release extraction
+
+ZeroJ has not been released yet. The current BLS12-381 classes are only used
+inside ZeroJ modules, examples, and use-case documentation, so package-level
+source compatibility is not a primary constraint.
+
+Correctness and standards compatibility are more important than preserving the
+current Groth16 package locations. If moving implementation code into
+`zeroj-bls12381` produces a cleaner and less duplicated design, update the
+internal Groth16, PlonK, examples, and use-case imports directly.
+
+The preferred migration path is:
+
+1. introduce `zeroj-bls12381` with neutral APIs
+2. move reusable implementation behind those APIs where practical
+3. update Groth16/PlonK internals to use the neutral module
+4. update `zeroj-examples` and `zeroj-usecases` imports directly
+5. add compatibility wrappers only if they materially reduce migration risk
+
+### 3. Add a provider SPI
+
+Define a small BLS12-381 provider boundary so higher-level protocols can choose
+between implementations.
+
+The default provider is the pure Java provider from `zeroj-bls12381`.
+Alternative providers must be explicit opt-ins. ZeroJ must not silently switch
+to native or WASM providers just because an optional module is on the classpath.
+
+Provider implementations must pass the same conformance tests and vector suites
+before they are marked supported.
+
+### 4. Add `zeroj-bls12381-wasm` as optional
+
+Create an optional module named:
+
+```
+zeroj-bls12381-wasm
+```
+
+This module wraps a Rust BLS12-381 implementation compiled to
+`wasm32-unknown-unknown` and executed through Chicory. The first candidate is
+`zkcrypto/bls12_381` because it is pure Rust and compiled cleanly in the spike.
+`ark-bls12-381` remains a backup candidate. Native `blst`-to-WASM is deferred
+until its build pipeline can be made reliable.
+
+The WASM provider is not the default. It is an optional provider and benchmark
+target.
+
+The WASM ABI should avoid very small cross-boundary calls such as individual
+field additions. Prefer coarse operations such as:
+
+- point decode/encode
+- hash-to-G1 or hash-to-G2
+- G1/G2 scalar multiplication
+- G1 multi-scalar multiplication
+- pairing product check
+- batched generator derivation
+
+For protocols such as BBS, a high-level WASM backend that performs full
+`sign`, `verify`, `deriveProof`, and `verifyProof` inside WASM may still be
+faster than a low-level primitive provider. This ADR does not require BBS to use
+the low-level WASM provider.
+
+### 5. Keep `blst` support explicit
+
+Native `blst` remains valuable for performance. ZeroJ already has a
+`zeroj-blst` module for verifier pairing operations, so native provider support
+can live there as an explicit `Bls12381Provider` implementation instead of
+adding another module immediately. It remains an explicit user opt-in because
+native loading has platform and packaging implications.
+
+## Consequences
+
+### Easier
+
+- BBS can target a clean BLS12-381 provider boundary instead of depending on
+ Groth16 package internals.
+- Groth16, PlonK, KZG, and BBS can share one standards-oriented primitive
+ module.
+- Pure Java remains the portable default with no native or WASM runtime
+ requirement.
+- WASM and native providers can be added and benchmarked without changing BBS
+ public APIs.
+
+### Harder
+
+- Extracting shared code from verifier/prover modules requires broad internal
+ import updates.
+- The provider SPI must be small enough to maintain but complete enough for
+ CFRG BBS and future protocols.
+- Multiple providers increase the conformance test burden.
+- WASM performance is not guaranteed to beat optimized JVM code, especially if
+ calls are too fine-grained.
+
+### Neutral
+
+- Existing BBS WASM work from ADR-0017 remains valid as an incubating backend.
+- Existing `zeroj-blst` can expose native-backed BLS12-381 provider operations
+ while remaining an explicit optional dependency.
+- On-chain verifier modules are unaffected by this ADR.
+
+## Test Plan
+
+- Unit tests for `zeroj-bls12381`:
+ - field arithmetic against existing ZeroJ tests
+ - G1/G2 generator, identity, addition, scalar multiplication, and subgroup
+ checks
+ - pairing product checks, including `e(P, Q) * e(-P, Q) == 1`
+ - compressed and uncompressed point encode/decode round trips
+ - RFC9380 hash-to-curve vectors for BLS12-381 G1 and G2
+- Integration migration tests:
+ - existing Groth16 and PlonK BLS12-381 tests continue to pass
+ - examples and use-case modules compile against the neutral module
+- WASM provider tests:
+ - Chicory 1.7.5 loads the WASM artifact
+ - the module exports memory and the expected ABI version
+ - exported operations have no unexpected host imports
+ - outputs match the pure Java provider for shared vectors
+- BBS follow-up tests:
+ - CFRG BBS draft vectors pass with the pure Java provider first
+ - optional WASM/native providers must pass the same vectors before being
+ advertised as supported for BBS
+
+## Implementation Plan
+
+1. Create `zeroj-bls12381` and move the existing pure Java BLS12-381
+ primitives behind neutral packages where practical.
+2. Update current Groth16/PlonK imports to use the new packages directly.
+3. Update Groth16/PlonK internals to depend on `zeroj-bls12381`.
+4. Add the provider SPI and make pure Java the default provider.
+5. Add `zeroj-bls12381-wasm` with a minimal Chicory smoke test and vector
+ equality tests against the pure Java provider.
+6. Resume CFRG BBS implementation on top of the provider boundary.
+
+## Implementation Status
+
+Implemented on 2026-05-07:
+
+- `zeroj-bls12381` owns the shared pure Java BLS12-381 field, curve, pairing,
+ generator, codec, and provider SPI classes.
+- Groth16, PlonK, KZG, examples, and on-chain test utilities use the neutral
+ BLS12-381 packages instead of Groth16 verifier or `zeroj-crypto` internals.
+- `zeroj-bls12381-wasm` provides an explicit Chicory-backed provider using
+ Rust `zkcrypto/bls12_381` compiled to `wasm32-unknown-unknown`.
+- The WASM ABI currently exposes generator retrieval, G1/G2 scalar
+ multiplication, and pairing product checks as coarse operations.
+- Shared codecs validate uncompressed points for curve membership and
+ prime-order subgroup membership by default, with explicit unchecked decode
+ helpers for trusted internal boundaries.
+- Shared codecs support compressed and uncompressed G1/G2 encodings with
+ round-trip tests, infinity handling, curve-membership checks, and subgroup
+ rejection tests.
+- The provider SPI covers the BBS-required low-level boundary: G1/G2 identity,
+ add, negate, scalar multiplication, subgroup validation, compressed and
+ uncompressed codecs, RFC9380 hash-to-curve helpers, encode-to-curve helpers,
+ and scalar hashing.
+- `zeroj-bls12381` implements RFC9380 hash-to-curve and encode-to-curve for
+ BLS12-381 G1/G2 with official vector coverage for the suites required by
+ CFRG BBS.
+- Provider scalar multiplication reduces signed `BigInteger` inputs modulo the
+ BLS12-381 scalar-field order so pure Java and WASM providers have the same
+ scalar-domain behavior.
+- `zeroj-bls12381-wasm` has explicit ABI conformance tests for no host imports,
+ expected exports, wrong-length inputs, invalid point bytes, and repeated
+ error handling.
+- The WASM Rust crate is built from source with a committed Cargo lockfile and
+ a pinned Rust toolchain file; the generated `.wasm` is packaged by Gradle
+ rather than checked in.
+- `zeroj-blst` exposes `BlstBls12381Provider`, an explicit native-backed
+ provider that implements the shared BLS12-381 provider SPI.
+- BBS provider conformance tests exercise official draft-10 vectors through the
+ pure Java, WASM, and blst BLS providers.
+- The BOM and Gradle settings include both new modules.
+
+Future BLS12-381 providers must pass the same provider and BBS conformance
+vectors before being advertised as BBS-capable.
+
+## Risks
+
+| Risk | Severity | Mitigation |
+|---|---:|---|
+| Accidental behavior change during extraction | High | Move code in small slices and keep existing Groth16/PlonK tests passing |
+| Provider SPI becomes too low-level and slow for WASM | Medium | Use batched/coarse primitive operations; allow high-level protocol backends |
+| WASM provider is slower than pure Java | Medium | Keep pure Java default; benchmark before recommending WASM |
+| Hash-to-curve incompatibility | High | Require RFC9380 vectors before using providers for BBS |
+| Native/WASM provider auto-selection surprises users | Medium | Require explicit provider selection |
+| Refactor delays BBS | Medium | Start with direct internal moves; use adapters only if extraction grows too large |
+
+## References
+
+- ADR-0007: Multi-Module Structure and Boundaries
+- ADR-0012: Pure Java Provers for Groth16 and PlonK
+- ADR-0017: BBS Selective Disclosure via Rust WASM and Chicory
+- Rust `bls12_381` crate:
+- Rust `ark-bls12-381` crate:
+- Rust `blst` crate:
+- Chicory docs:
+- RFC 9380 hash-to-curve:
diff --git a/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
new file mode 100644
index 0000000..7dbe37b
--- /dev/null
+++ b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
@@ -0,0 +1,398 @@
+# ADR-0019: CFRG BBS Pure Java and Optional WASM Providers
+
+## Status
+Accepted
+
+## Date
+2026-05-07
+
+## Context
+
+ADR-0017 planned BBS selective disclosure as a WASM-first feature backed by an
+arkworks `bbs_plus` implementation. That direction is no longer the right
+default. ADR-0018 has extracted ZeroJ's reusable BLS12-381 primitives into
+`zeroj-bls12381`, including point encodings, subgroup checks, pairing product
+checks, scalar hashing, and RFC9380 hash-to-curve support. This makes a
+standards-oriented pure Java BBS implementation feasible.
+
+The existing `zeroj-bbs/` scaffold is also not the target shape. It uses a
+ZeroJ-specific BBS+ suite and CBOR ABI, not the CFRG BBS interface and octet
+serialization from `draft-irtf-cfrg-bbs-signatures-10`. We should replace that
+incubating code instead of evolving it.
+
+The target standard is pinned to:
+
+```
+draft-irtf-cfrg-bbs-signatures-10
+```
+
+That draft defines the high-level interface operations `KeyGen`, `SkToPk`,
+`Sign`, `Verify`, `ProofGen`, and `ProofVerify`, plus the core operations and
+utility operations needed to make those interfaces vector-compatible. It also
+defines BLS12-381 SHAKE-256 and SHA-256 ciphersuites and includes test vectors.
+
+## Decision
+
+### 1. Replace the current BBS scaffold
+
+Remove the existing experimental `zeroj-bbs/` directory and its nested
+`wasm/` and `rust/` layout during implementation. Create new BBS modules with
+the same shape as the BLS modules:
+
+```
+zeroj-bbs/
+ build.gradle
+ src/main/java/com/bloxbean/cardano/zeroj/bbs/...
+ src/test/resources/cfrg-bbs/draft10/...
+
+zeroj-bbs-wasm/
+ build.gradle
+ rust/
+ src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/...
+```
+
+`zeroj-bbs` is the default portable implementation. `zeroj-bbs-wasm` is an
+optional provider and benchmark target.
+
+### 2. Make pure Java the default BBS provider
+
+`zeroj-bbs` depends on `zeroj-bls12381` and implements CFRG BBS directly in
+Java. It must not depend on the WASM module.
+
+The public Java API should be standards-oriented and provider-backed:
+
+- `BbsCiphersuite`
+- `BbsSecretKey`
+- `BbsPublicKey`
+- `BbsKeyPair`
+- `BbsSignature`
+- `BbsProof`
+- `BbsPresentation`
+- `BbsProvider`
+- `BbsProviders`
+- `BbsService`
+
+`BbsProviders.pureJava()` is the default. Alternate providers are explicit
+opt-ins. ZeroJ must not silently switch to WASM because `zeroj-bbs-wasm` is on
+the classpath.
+
+### 3. Implement the CFRG draft-10 interface, not the older BBS+ API
+
+The implementation target is the draft-10 interface algorithms:
+
+- `KeyGen`
+- `SkToPk`
+- `Sign`
+- `Verify`
+- `ProofGen`
+- `ProofVerify`
+
+The implementation must also expose or test the draft utility operations needed
+for vector compatibility:
+
+- secret-key derivation from key material and key info
+- public-key derivation
+- message-to-scalar mapping
+- generator derivation
+- domain calculation
+- challenge calculation
+- random scalar derivation for test vectors
+- scalar, point, signature, proof, and public-key octet serialization
+
+Core cryptographic bytes must follow the CFRG draft octet formats. ZeroJ may
+define a small CBOR envelope for `ZkProofEnvelope` integration, but that
+envelope must wrap draft-compatible BBS proof bytes rather than replacing the
+draft serialization.
+
+### 4. Ciphersuite support
+
+Implement the BLS12-381 SHA-256 ciphersuite first:
+
+```
+BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_
+```
+
+This is the lowest-risk first target because `zeroj-bls12381` already has the
+required RFC9380 XMD SHA-256 hash-to-G1 support and test vectors.
+
+Support the SHAKE-256 ciphersuite as a second milestone:
+
+```
+BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_
+```
+
+ZeroJ must not advertise full draft-10 ciphersuite coverage until both
+ciphersuites pass the corresponding draft vectors. If SHAKE-256 requires new
+XOF support in `zeroj-bls12381`, add that as a prerequisite before enabling
+the suite.
+
+### 5. Constant-time secret-scalar boundary
+
+BBS uses secret keys, proof randomness, and hidden-message blinding scalars.
+The implementation must not route those through provider methods documented
+only for public scalar multiplication.
+
+Before production use, `zeroj-bbs` must either:
+
+- add constant-time secret-scalar operations to `zeroj-bls12381` and use them
+ for BBS secret-dependent scalar multiplication, or
+- keep the BBS module clearly marked experimental until a side-channel review
+ is complete.
+
+Correct vector compatibility is required first, but a constant-time contract is
+required before advertising the pure Java implementation as production-ready
+for secret-bearing BBS workflows.
+
+Implementation note: the follow-up implementation added explicit
+`g1SecretScalarMul` and `g2SecretScalarMul` provider methods, wired BBS
+`SkToPk`, signing, proof blinding, proof randomness, and hidden-message
+commitments through those methods, and replaced BBS secret scalar inversions
+with Montgomery-form scalar-field inversion. The JVM implementation uses a
+fixed-schedule Jacobian multiplication path; high-value deployments should
+still perform an environment-specific side-channel review.
+
+### 6. BLS-provider swapping happens in `zeroj-bbs`, not in separate modules
+
+Native `blst` is exposed as a BLS12-381 provider from `zeroj-blst`. The WASM
+BLS12-381 primitives are exposed from `zeroj-bls12381-wasm`. BBS can use either
+through explicit BLS provider selection without adding a separate
+`zeroj-bbs-blst` or `zeroj-bbs-wasm-bls` module:
+
+```java
+// Java BBS + WASM BLS primitives (hybrid)
+BbsProviders.withBlsProvider(ciphersuite, WasmBls12381Provider.createDefault())
+
+// Java BBS + native blst BLS primitives
+BbsProviders.withBlsProvider(ciphersuite, BlstBls12381Provider.createDefault())
+```
+
+The main `zeroj-bbs` module must continue to depend only on the shared
+`zeroj-bls12381` API; WASM and blst remain user-selected optional dependencies
+that callers add to their classpath when they want them. The BLS provider
+conformance suite in `zeroj-bbs` already parameterizes across `(pure-java BLS,
+WASM BLS, blst BLS) × (SHA-256, SHAKE-256)` to gate any new BLS backend on the
+same draft-10 fixtures.
+
+A separate "hybrid Java BBS + WASM BLS" module would only re-export a one-line
+factory that callers can write themselves via `withBlsProvider`. It would add
+audit surface and packaging cost without adding capability. We therefore do not
+ship such a module.
+
+### 7. `zeroj-bbs-wasm` is reserved for the full Rust-WASM BBS provider
+
+`zeroj-bbs-wasm` implements the same `BbsProvider` SPI as an explicit opt-in,
+but the algorithm itself runs entirely inside WebAssembly. The Rust BBS crate
+is compiled to `wasm32-unknown-unknown` and executed through Chicory; ZeroJ's
+Java code only marshals requests, results, and errors across the WASM boundary.
+This eliminates the cross-boundary calls that the hybrid path incurs per
+pairing or per scalar multiplication.
+
+The coarse Rust ABI is:
+
+- `zeroj_bbs_keygen`
+- `zeroj_bbs_sk_to_pk`
+- `zeroj_bbs_sign`
+- `zeroj_bbs_verify`
+- `zeroj_bbs_proof_gen`
+- `zeroj_bbs_proof_verify`
+
+The first Rust candidate is `zkryptium` because it implements CFRG draft-10
+directly and supports both BLS12-381-SHA-256 and BLS12-381-SHAKE-256
+ciphersuites. It must compile cleanly to `wasm32-unknown-unknown`, must not
+introduce unexpected host imports (no `getrandom`/`wasm-bindgen` shims), and
+must pass the pinned draft-10 vectors byte-for-byte. If it does not meet those
+gates, implement the Rust provider against a lower-level BLS12-381 crate
+instead. Do not patch the older `bbs_plus` crate or `mattrglobal/pairing_crypto`
+(BBS draft-03 / BBS+) into a draft-10 shape.
+
+The WASM ABI must mirror the hardened `zeroj-bls12381-wasm` pattern:
+
+- committed `Cargo.lock`
+- pinned `rust-toolchain.toml`
+- generated `.wasm` built by Gradle, not checked in
+- exported memory plus explicit `alloc` and `dealloc`
+- typed Java exceptions for malformed responses
+- tests for alloc/dealloc balance on error paths
+- response allocation length captured before length validation, so malformed
+ responses still get freed
+
+The full Rust-WASM BBS provider requires a real RNG for `proof_gen` (BBS
+proof randomness is essential to the zero-knowledge property — a deterministic
+seed would leak the secret across two presentations of the same signature). The
+zkryptium 0.6.1 candidate does not expose a public API for caller-supplied
+random scalars, so the WASM module exposes exactly **one** named host import:
+
+```
+env.zeroj_host_getrandom(ptr: i32, len: i32) -> i32 // 0 = ok, !=0 = error
+```
+
+The Java side wires this to `java.security.SecureRandom` via Chicory. No other
+host imports are permitted; tests must assert that exactly this one import is
+present and nothing else.
+
+CFRG mocked-RNG proof byte-equality is retained only on the pure-Java provider
+(`PureJavaBbsProvider`), where `BbsBlsProviderConformanceTest` already gates
+all 30 proof × 2 ciphersuite fixtures with deterministic scalars. The
+full-WASM provider verifies proof correctness via roundtrip (`proof_gen` then
+`proof_verify`) plus byte-exact tests on the deterministic operations
+(`keygen`, `sk_to_pk`, `sign`, `verify`, `proof_verify` on a known fixture
+proof). This keeps one byte-exact algorithm gate upstream without doubling
+fixture maintenance.
+
+### 8. ZeroJ verifier integration
+
+`zeroj-bbs` should provide a BBS verifier for ZeroJ's verifier APIs:
+
+- proof system: `ProofSystemId.BBS`
+- curve: `CurveId.BLS12_381`
+- proof format: `bbs-cfrg-draft10-presentation-cbor-v1`
+
+The verifier checks only cryptographic correctness. Issuer trust, schema
+semantics, expiration, revocation, holder binding, and disclosure policy remain
+application policy concerns, consistent with ADR-0006.
+
+Provider selection remains explicit. ServiceLoader may discover the
+`ZkVerifier`, but it must not silently switch the underlying BBS provider from
+pure Java to WASM.
+
+## Consequences
+
+### Easier
+
+- BBS follows the same architecture as BLS: portable pure Java default plus an
+ optional WASM backend.
+- CFRG vector compatibility becomes the primary correctness gate.
+- `zeroj-bbs` can be used without Rust, WASM, native code, or Chicory.
+- WASM can be benchmarked and improved independently.
+
+### Harder
+
+- The pure Java implementation must implement the full draft algorithm instead
+ of wrapping an existing BBS+ library.
+- Constant-time secret-scalar handling must be addressed explicitly.
+- Supporting both SHA-256 and SHAKE-256 ciphersuites adds hash/XOF work.
+- Multiple BLS providers increase the conformance-test burden.
+
+### Neutral
+
+- No Cardano on-chain verifier is implied.
+- W3C Data Integrity `bbs-2023` packaging can be layered later on top of the
+ CFRG-compatible BBS core.
+- Blind BBS signatures and per-verifier linkability are out of scope for this
+ ADR.
+
+## Test Plan
+
+### Pure Java tests
+
+- Import draft-10 vectors for:
+ - key generation
+ - public key derivation
+ - message-to-scalar mapping
+ - generator derivation
+ - signature generation
+ - signature verification
+ - proof generation
+ - proof verification
+ - hash-to-scalar cases
+ - Appendix D.1 SHAKE-256 signature and proof fixtures from the official draft
+ JSON set
+ - Appendix D.2 SHA-256 signature and proof fixtures from the official draft
+ JSON set
+- Add negative tests for:
+ - tampered signature
+ - tampered proof
+ - wrong public key
+ - wrong header
+ - wrong presentation header
+ - wrong revealed message
+ - duplicate, unsorted, and out-of-range revealed indexes
+ - invalid scalar, G1, and G2 encodings
+ - subgroup rejection on public keys and proof points
+- Add deterministic mocked-random scalar tests for draft proof vectors.
+- Add randomized round-trip tests for sign, verify, proof generation, and proof
+ verification after the draft vectors pass.
+
+### Provider conformance tests
+
+- Define one shared conformance suite in `zeroj-bbs`.
+- Run the same suite against BBS with selected BLS providers:
+ - pure Java BLS from `zeroj-bls12381`
+ - WASM BLS from `zeroj-bls12381-wasm`
+ - native blst BLS from `zeroj-blst`
+- A provider cannot be advertised as BBS-capable until it passes the shared
+ draft vectors.
+
+### WASM tests
+
+- Chicory loads the generated WASM artifact.
+- The WASM module exports the expected ABI version and operations.
+- The module imports exactly one host function (`env.zeroj_host_getrandom`)
+ and nothing else.
+- Deterministic operations (`KeyGen`, `SkToPk`, `Sign`) match the official
+ CFRG draft-10 fixture bytes byte-for-byte for both ciphersuites.
+- `proof_verify` accepts the official CFRG draft-10 proof bytes for both
+ ciphersuites; tampered/modified fixtures are rejected.
+- `proof_gen` is verified by roundtrip (the generated proof must verify under
+ the same WASM provider) rather than by byte-equality, because the full-WASM
+ path consumes real entropy through the host RNG import.
+- Malformed requests and malformed responses map to typed Java exceptions.
+- Repeated WASM errors do not leak response buffers.
+- The host RNG import is the only crossing where the Rust crate can obtain
+ entropy; the per-call `SecureRandom` supplied to `BbsProvider.proofGen`
+ must drive that crossing for each invocation.
+
+### Integration tests
+
+- `BbsService` signs, verifies, derives a presentation, and verifies the
+ presentation using the pure Java provider.
+- `ZkProofEnvelope` can carry a BBS presentation.
+- `VerifierOrchestrator` verifies a BBS presentation envelope.
+- Wrong proof system, curve, proof format, verification material, and public
+ inputs reject cleanly.
+
+## Implementation Plan
+
+1. Supersede ADR-0017 and delete the old experimental `zeroj-bbs/` scaffold.
+2. Create `zeroj-bbs` with the public API, `BbsProvider` SPI, and pure Java
+ provider shell.
+3. Add CFRG draft-10 vector resources and a vector loader.
+4. Implement draft serialization utilities, scalar utilities, and ciphersuite
+ metadata.
+5. Implement `KeyGen`, `SkToPk`, message-to-scalar mapping, and generator
+ derivation; pass draft vectors.
+6. Implement `Sign` and `Verify`; pass signature vectors and negative tests.
+7. Implement `ProofGen` and `ProofVerify`; pass proof vectors and negative
+ tests.
+8. Add `BbsService`, presentation wrappers, and ZeroJ verifier integration.
+9. Run the shared conformance suite against pure Java, WASM, and blst BLS
+ providers using `BbsService.withBlsProvider(...)` from `zeroj-bbs`.
+10. Create `zeroj-bbs-wasm` as the full Rust-WASM BBS provider: Rust candidate
+ (`zkryptium` first) compiled to `wasm32-unknown-unknown`, coarse `zeroj_bbs_*`
+ ABI, Java client mirroring the hardened `zeroj-bls12381-wasm` pattern,
+ no-host-imports + alloc/dealloc balance + malformed-response leak tests,
+ and extend the conformance suite to gate on the new path.
+11. Add documentation and examples for selective disclosure workflows.
+
+## Risks
+
+| Risk | Severity | Mitigation |
+|---|---:|---|
+| Draft-10 changes before RFC publication | Medium | Pin proof format and vectors to draft-10; add a new suite for later drafts |
+| Pure Java side-channel leakage | Medium | Secret-scalar provider boundary is in place; require environment-specific side-channel review for high-value deployments |
+| Incorrect generator/domain/challenge serialization | High | Gate each utility on draft vectors before implementing higher layers |
+| Non-default providers drift from pure Java | Medium | Shared provider conformance suite and exact same CFRG vectors |
+| Rust crate claims draft-10 but differs in details | Medium | Treat Rust crates as candidates only; vectors decide support |
+| Rust crate pulls in additional host imports beyond `env.zeroj_host_getrandom` | Medium | Single-import assertion in the WASM hardening test rejects any extra import; if a candidate cannot be configured to call only `getrandom` on the host side, drop to a lower-level BLS crate per §7. |
+| Host RNG quality on caller's JVM is weaker than expected | Low | Per-call `SecureRandom` is injectable through the `BbsProvider.proofGen` SPI; production callers can pass a `SecureRandom.getInstanceStrong()` or a hardware-backed instance. |
+| Users confuse CFRG core with W3C Data Integrity packaging | Medium | Keep proof format names explicit and document policy/package boundaries |
+
+## References
+
+- ADR-0006: Separation of Crypto and Policy Verification
+- ADR-0018: Shared BLS12-381 Primitives and Optional WASM Provider
+- BBS draft-10:
+- BBS draft datatracker entry:
+- RFC 9380 hash-to-curve:
+- ZKryptium Rust crate candidate:
+- `mattrglobal/pairing_crypto` (BBS draft-03, BBS+ flavor — NOT a draft-10 candidate):
diff --git a/docs/pure-java-prover-guide.md b/docs/pure-java-prover-guide.md
index 28a57ce..68130fa 100644
--- a/docs/pure-java-prover-guide.md
+++ b/docs/pure-java-prover-guide.md
@@ -75,7 +75,8 @@ public class SecretMultiplierCircuit implements CircuitSpec {
import com.bloxbean.cardano.zeroj.api.CurveId;
import com.bloxbean.cardano.zeroj.crypto.groth16.*;
import com.bloxbean.cardano.zeroj.crypto.setup.*;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
// Compile circuit
var circuit = SecretMultiplierCircuit.build();
diff --git a/settings.gradle b/settings.gradle
index 055484b..54def92 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -7,6 +7,7 @@ include 'zeroj-backend-spi'
include 'zeroj-verifier-core'
include 'zeroj-verifier-groth16'
include 'zeroj-verifier-plonk'
+include 'zeroj-bls12381'
include 'zeroj-blst'
include 'zeroj-test-vectors'
include 'zeroj-submission'
@@ -22,6 +23,10 @@ include 'zeroj-onchain-julc'
include 'zeroj-examples'
include 'zeroj-bom'
+include 'zeroj-bls12381-wasm'
+include 'zeroj-bbs-wasm'
+include 'zeroj-bbs'
+
// === Incubator modules (experimental, alternative backends) ===
include 'zeroj-prover-rapidsnark'
project(':zeroj-prover-rapidsnark').projectDir = file('incubator/zeroj-prover-rapidsnark')
diff --git a/zeroj-api/src/main/java/com/bloxbean/cardano/zeroj/api/ProofSystemId.java b/zeroj-api/src/main/java/com/bloxbean/cardano/zeroj/api/ProofSystemId.java
index 2a1d1cf..d09cda2 100644
--- a/zeroj-api/src/main/java/com/bloxbean/cardano/zeroj/api/ProofSystemId.java
+++ b/zeroj-api/src/main/java/com/bloxbean/cardano/zeroj/api/ProofSystemId.java
@@ -7,7 +7,8 @@ public enum ProofSystemId {
GROTH16("groth16"),
PLONK("plonk"),
FFLONK("fflonk"),
- HALO2("halo2");
+ HALO2("halo2"),
+ BBS("bbs");
private final String value;
diff --git a/zeroj-bbs-wasm/README.md b/zeroj-bbs-wasm/README.md
new file mode 100644
index 0000000..7cf8667
--- /dev/null
+++ b/zeroj-bbs-wasm/README.md
@@ -0,0 +1,131 @@
+# zeroj-bbs-wasm
+
+Full Rust-WASM CFRG BBS draft-10 provider. The entire BBS algorithm runs
+inside WebAssembly via [zkryptium 0.6.1](https://docs.rs/zkryptium) compiled
+to `wasm32-unknown-unknown`, executed through Chicory. ZeroJ's Java layer
+only serializes requests, parses responses, and supplies entropy via a single
+documented host import.
+
+See [ADR-0019](../docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md) §7
+for the design rationale.
+
+## When to use this module
+
+Use the full Rust-WASM provider when you want the BBS algorithm to run
+end-to-end inside a Rust-derived WebAssembly module and are OK with shipping
+a small WebAssembly artifact. Signing and verification use coarse WASM calls
+against a long-lived instance. Proof generation also uses a coarse call, but
+ZeroJ intentionally creates a fresh WASM instance for each `proofGen` call so
+the caller-supplied `SecureRandom` is honored for every proof.
+
+```java
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bbs.BbsService;
+import com.bloxbean.cardano.zeroj.bbs.wasm.WasmBbsProvider;
+
+var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+var service = new BbsService(provider);
+
+var keyPair = service.keyPair(keyMaterial, keyInfo);
+var signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+var presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, presentationHeader,
+ new int[]{0, 2});
+boolean valid = service.verifyPresentation(keyPair.publicKey(), presentation);
+```
+
+## When NOT to use this module
+
+If you only need to swap the BLS12-381 primitive layer (pairings, scalar
+mul) — for example to benchmark WASM-backed BLS while keeping the BBS
+algorithm in Java — use the hybrid path directly from `zeroj-bbs`:
+
+```java
+// Java BBS algorithm + WASM BLS primitives:
+var service = BbsService.withBlsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ WasmBls12381Provider.createDefault());
+
+// Java BBS algorithm + native blst BLS primitives:
+var service = BbsService.withBlsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ BlstBls12381Provider.createDefault());
+```
+
+`zeroj-bbs/src/test/java/.../BbsBlsProviderConformanceTest` already runs the
+full CFRG draft-10 fixture suite (10 signatures + 15 proofs + keypair + h2s
++ generators + MapMessageToScalar + mockedRng, both ciphersuites) across all
+three BLS providers via `withBlsProvider`. No separate hybrid module exists.
+
+## ABI
+
+The WASM module exposes:
+
+- `zeroj_bbs_version() -> i32` (ABI version 1)
+- `alloc(len) -> *mut u8`
+- `dealloc(ptr, len)`
+- `zeroj_bbs_keygen(req_ptr, req_len) -> *mut u8`
+- `zeroj_bbs_sk_to_pk(req_ptr, req_len) -> *mut u8`
+- `zeroj_bbs_sign(req_ptr, req_len) -> *mut u8`
+- `zeroj_bbs_verify(req_ptr, req_len) -> *mut u8`
+- `zeroj_bbs_proof_gen(req_ptr, req_len) -> *mut u8`
+- `zeroj_bbs_proof_verify(req_ptr, req_len) -> *mut u8`
+
+Every response is laid out as `[u32 LE length | status byte | payload]`.
+Status `0` = success, status `1` = error (payload is UTF-8 error message).
+Callers free the response buffer with `dealloc(ptr, length + 4)`.
+
+The module imports exactly one host function:
+
+- `env.zeroj_host_getrandom(ptr: i32, len: i32) -> i32` — `0` on success,
+ non-zero on error. Java wires this to `java.security.SecureRandom`.
+
+A test (`wasmModule_hasExactlyOneImportAndExpectedExports`) asserts that no
+other imports are present.
+
+## Runtime behavior
+
+`keyGen`, `skToPk`, `sign`, `verify`, and `proofVerify` run on a long-lived
+WASM instance owned by the provider. These operations are deterministic and
+should never call the host RNG import.
+
+`proofGen` needs fresh zero-knowledge blinding randomness. The Rust dependency
+uses an internal thread-local RNG, so ZeroJ builds a transient WASM instance
+per `proofGen` call and wires that instance's `env.zeroj_host_getrandom`
+import to the `SecureRandom` supplied to the Java API call. This avoids
+silently reusing or bypassing the per-call random source after the first
+proof.
+
+The tradeoff is performance: `proofGen` includes WASM instantiation overhead
+in addition to the BBS proof computation. This is correctness-first behavior,
+not a protocol limitation. For high-throughput proof issuance, benchmark this
+provider under the target workload and compare it with `zeroj-bbs` using the
+native `zeroj-blst` BLS provider. A future optimization can remove the
+per-proof instantiation cost if the Rust dependency exposes an API that
+accepts caller-provided randomness directly.
+
+## Building
+
+The `.wasm` artifact is built on demand by Gradle during `processResources`.
+Install Rust with the pinned toolchain (`rust-toolchain.toml` selects
+`rustc 1.94.0` with the `wasm32-unknown-unknown` target). Gradle uses
+`~/.cargo/bin/cargo` by default; override via the `CARGO` environment
+variable if needed.
+
+```bash
+./gradlew :zeroj-bbs-wasm:build
+./gradlew :zeroj-bbs-wasm:test
+```
+
+## Trust boundary
+
+The CFRG draft-10 algorithm correctness is owned by zkryptium upstream and
+gated on the ZeroJ side by the pure-Java `PureJavaBbsProvider` running all
+30 CFRG mocked-RNG proof fixtures × 2 ciphersuites byte-for-byte in
+`BbsBlsProviderConformanceTest`. The WASM tests in this module focus on the
+ZeroJ-owned boundary: request encoding, response framing, error mapping,
+alloc/dealloc balance under failure, no-unexpected-host-imports, and a
+small "trust ladder" of byte-exact CFRG fixtures (keygen, sk_to_pk, sign)
+plus a proof_gen → proof_verify roundtrip. CFRG mocked-RNG proof
+byte-equality is not retestable in this module because `proof_gen` uses
+real RNG via the host import.
diff --git a/zeroj-bbs-wasm/build.gradle b/zeroj-bbs-wasm/build.gradle
new file mode 100644
index 0000000..3e404e2
--- /dev/null
+++ b/zeroj-bbs-wasm/build.gradle
@@ -0,0 +1,53 @@
+plugins {
+ id 'java-library'
+}
+
+description = 'ZeroJ full Rust-WASM CFRG BBS provider via zkryptium and Chicory'
+
+def bbsRustDir = project.file('rust')
+def bbsWasmTarget = bbsRustDir.toPath()
+ .resolve('target/wasm32-unknown-unknown/release/zeroj_bbs.wasm')
+ .toFile()
+def generatedWasmDir = layout.buildDirectory.dir('generated-resources/wasm')
+
+dependencies {
+ api project(':zeroj-bbs')
+
+ implementation 'com.dylibso.chicory:runtime:1.7.5'
+ implementation 'com.dylibso.chicory:wasm:1.7.5'
+}
+
+def cargoBinary = System.getenv('CARGO') ?: (System.getProperty('user.home') + '/.cargo/bin/cargo')
+
+tasks.register('buildBbsWasm', Exec) {
+ description = 'Builds the zkryptium CFRG BBS Rust crate as wasm32-unknown-unknown'
+ group = 'build'
+ workingDir = bbsRustDir
+ commandLine cargoBinary, 'build', '--release', '--target', 'wasm32-unknown-unknown'
+ inputs.files(fileTree(bbsRustDir) {
+ include 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', 'src/**/*.rs'
+ })
+ outputs.file(bbsWasmTarget)
+}
+
+tasks.register('copyBbsWasm', Copy) {
+ dependsOn tasks.named('buildBbsWasm')
+ from(bbsWasmTarget)
+ into(generatedWasmDir.map { it.dir('zeroj-bbs-wasm') })
+}
+
+processResources {
+ dependsOn tasks.named('copyBbsWasm')
+ from(generatedWasmDir)
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ pom {
+ name = 'ZeroJ BBS WASM'
+ description = 'Optional CFRG BBS provider with the entire BBS algorithm running inside WebAssembly via zkryptium and Chicory'
+ }
+ }
+ }
+}
diff --git a/zeroj-bbs-wasm/rust/Cargo.lock b/zeroj-bbs-wasm/rust/Cargo.lock
new file mode 100644
index 0000000..3066ade
--- /dev/null
+++ b/zeroj-bbs-wasm/rust/Cargo.lock
@@ -0,0 +1,475 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arrayref"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bls12_381_plus"
+version = "0.8.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa37cf2a8c96054d2dc3d708efe35cc0347014af0d30b86c736b4388ff8491c"
+dependencies = [
+ "arrayref",
+ "elliptic-curve",
+ "ff",
+ "group",
+ "hex",
+ "pairing",
+ "rand_core",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "bitvec",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "generic-array"
+version = "0.14.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "pairing"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
+dependencies = [
+ "group",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zeroj-bbs-wasm"
+version = "0.1.0"
+dependencies = [
+ "elliptic-curve",
+ "getrandom",
+ "zkryptium",
+]
+
+[[package]]
+name = "zkryptium"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39159a1cd33b28bad7c3502528f77da679dd6f45a055581f5ac56954c458c6e5"
+dependencies = [
+ "bls12_381_plus",
+ "digest",
+ "elliptic-curve",
+ "ff",
+ "group",
+ "hex",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sha3",
+ "thiserror",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/zeroj-bbs-wasm/rust/Cargo.toml b/zeroj-bbs-wasm/rust/Cargo.toml
new file mode 100644
index 0000000..a6c7366
--- /dev/null
+++ b/zeroj-bbs-wasm/rust/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "zeroj-bbs-wasm"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+name = "zeroj_bbs"
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+zkryptium = { version = "0.6.1", default-features = false, features = ["bbsplus"] }
+getrandom = { version = "0.2", features = ["custom"] }
+elliptic-curve = { version = "0.13", default-features = false, features = ["hash2curve"] }
+
+[profile.release]
+opt-level = "z"
+lto = true
+panic = "abort"
+codegen-units = 1
diff --git a/zeroj-bbs-wasm/rust/rust-toolchain.toml b/zeroj-bbs-wasm/rust/rust-toolchain.toml
new file mode 100644
index 0000000..6596900
--- /dev/null
+++ b/zeroj-bbs-wasm/rust/rust-toolchain.toml
@@ -0,0 +1,4 @@
+[toolchain]
+channel = "1.94.0"
+targets = ["wasm32-unknown-unknown"]
+profile = "minimal"
diff --git a/zeroj-bbs-wasm/rust/src/lib.rs b/zeroj-bbs-wasm/rust/src/lib.rs
new file mode 100644
index 0000000..8e78bf6
--- /dev/null
+++ b/zeroj-bbs-wasm/rust/src/lib.rs
@@ -0,0 +1,453 @@
+//! ZeroJ CFRG BBS draft-10 provider — full BBS algorithm running inside
+//! WebAssembly via zkryptium 0.6.1.
+//!
+//! ABI: see ADR-0019 §7.
+//!
+//! Memory model: every response is laid out as
+//! [u32 LE total_payload_len | status_byte | payload_bytes]
+//! Status byte is 0 for success, 1 for error. On error, payload is a UTF-8
+//! error message. Caller reads the 4-byte length, then reads the status +
+//! payload, then frees the buffer with `dealloc(ptr, length + 4)`.
+//!
+//! Single host import: `env.zeroj_host_getrandom(ptr, len) -> i32` (0 = ok).
+//! All other operations are self-contained.
+
+use std::{mem, slice};
+
+use elliptic_curve::hash2curve::ExpandMsg;
+use zkryptium::{
+ bbsplus::{
+ ciphersuites::{BbsCiphersuite, Bls12381Sha256, Bls12381Shake256},
+ keys::{BBSplusPublicKey, BBSplusSecretKey},
+ },
+ keys::pair::KeyPair,
+ schemes::{
+ algorithms::BBSplus,
+ generics::{PoKSignature, Signature},
+ },
+};
+
+// ---------- Host import ---------------------------------------------------
+
+extern "C" {
+ fn zeroj_host_getrandom(ptr: *mut u8, len: usize) -> i32;
+}
+
+fn host_backed_getrandom(buf: &mut [u8]) -> Result<(), getrandom::Error> {
+ if buf.is_empty() {
+ return Ok(());
+ }
+ let rc = unsafe { zeroj_host_getrandom(buf.as_mut_ptr(), buf.len()) };
+ if rc == 0 {
+ Ok(())
+ } else {
+ Err(getrandom::Error::FAILED_RDRAND)
+ }
+}
+getrandom::register_custom_getrandom!(host_backed_getrandom);
+
+// ---------- Memory primitives ---------------------------------------------
+
+#[no_mangle]
+pub extern "C" fn alloc(len: usize) -> *mut u8 {
+ let mut buf = Vec::with_capacity(len);
+ let ptr = buf.as_mut_ptr();
+ mem::forget(buf);
+ ptr
+}
+
+#[no_mangle]
+pub extern "C" fn dealloc(ptr: *mut u8, len: usize) {
+ if ptr.is_null() || len == 0 {
+ return;
+ }
+ unsafe {
+ let _ = Vec::from_raw_parts(ptr, len, len);
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_version() -> u32 {
+ 1
+}
+
+// ---------- ABI entrypoints ----------------------------------------------
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_keygen(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, op_keygen)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_sk_to_pk(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, op_sk_to_pk)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_sign(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, op_sign)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_verify(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, op_verify)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_proof_gen(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, op_proof_gen)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bbs_proof_verify(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, op_proof_verify)
+}
+
+// ---------- Framing -------------------------------------------------------
+
+fn handle(ptr: *const u8, len: usize, op: F) -> *mut u8
+where
+ F: FnOnce(&[u8]) -> Result, String>,
+{
+ if ptr.is_null() {
+ return respond(Err("request pointer is null".into()));
+ }
+ let input = unsafe { slice::from_raw_parts(ptr, len) };
+ respond(op(input))
+}
+
+fn respond(result: Result, String>) -> *mut u8 {
+ let mut payload = Vec::new();
+ match result {
+ Ok(bytes) => {
+ payload.push(0);
+ payload.extend_from_slice(&bytes);
+ }
+ Err(message) => {
+ payload.push(1);
+ payload.extend_from_slice(message.as_bytes());
+ }
+ }
+ leak_response(payload)
+}
+
+fn leak_response(payload: Vec) -> *mut u8 {
+ let len = payload.len();
+ let mut buf = Vec::with_capacity(len + 4);
+ buf.extend_from_slice(&(len as u32).to_le_bytes());
+ buf.extend_from_slice(&payload);
+ let ptr = buf.as_mut_ptr();
+ mem::forget(buf);
+ ptr
+}
+
+// ---------- Ciphersuite dispatch ------------------------------------------
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum Suite {
+ Sha256,
+ Shake256,
+}
+
+fn read_suite(input: &[u8]) -> Result<(Suite, &[u8]), String> {
+ if input.is_empty() {
+ return Err("request is empty (expected ciphersuite byte)".into());
+ }
+ let suite = match input[0] {
+ 0 => Suite::Sha256,
+ 1 => Suite::Shake256,
+ other => return Err(format!("unknown ciphersuite byte: {other}")),
+ };
+ Ok((suite, &input[1..]))
+}
+
+macro_rules! with_suite {
+ ($suite:expr, $body:ident, $input:expr) => {
+ match $suite {
+ Suite::Sha256 => $body::($input),
+ Suite::Shake256 => $body::($input),
+ }
+ };
+}
+
+// ---------- Request decoder helpers ---------------------------------------
+
+struct Cursor<'a> {
+ buf: &'a [u8],
+ off: usize,
+}
+
+impl<'a> Cursor<'a> {
+ fn new(buf: &'a [u8]) -> Self {
+ Self { buf, off: 0 }
+ }
+
+ fn remaining(&self) -> usize {
+ self.buf.len() - self.off
+ }
+
+ fn need(&self, n: usize, label: &str) -> Result<(), String> {
+ if self.remaining() < n {
+ Err(format!(
+ "{label}: need {n} bytes, only {} remaining",
+ self.remaining()
+ ))
+ } else {
+ Ok(())
+ }
+ }
+
+ fn take(&mut self, n: usize, label: &str) -> Result<&'a [u8], String> {
+ self.need(n, label)?;
+ let out = &self.buf[self.off..self.off + n];
+ self.off += n;
+ Ok(out)
+ }
+
+ fn u32_le(&mut self, label: &str) -> Result {
+ let bytes = self.take(4, label)?;
+ Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
+ }
+
+ fn var_bytes(&mut self, label: &str) -> Result<&'a [u8], String> {
+ let len = self.u32_le(&format!("{label} length"))? as usize;
+ self.take(len, label)
+ }
+
+ fn message_list(&mut self) -> Result>, String> {
+ let count = self.u32_le("message count")? as usize;
+ if count > 1024 {
+ return Err(format!("message count {count} exceeds cap 1024"));
+ }
+ let mut out = Vec::with_capacity(count);
+ for i in 0..count {
+ let msg = self.var_bytes(&format!("message[{i}]"))?;
+ out.push(msg.to_vec());
+ }
+ Ok(out)
+ }
+
+ fn index_list(&mut self) -> Result, String> {
+ let count = self.u32_le("disclosed index count")? as usize;
+ if count > 1024 {
+ return Err(format!("disclosed index count {count} exceeds cap 1024"));
+ }
+ let mut out = Vec::with_capacity(count);
+ for i in 0..count {
+ let idx = self.u32_le(&format!("disclosed_index[{i}]"))? as usize;
+ out.push(idx);
+ }
+ Ok(out)
+ }
+
+ fn expect_eof(&self, label: &str) -> Result<(), String> {
+ if self.remaining() != 0 {
+ return Err(format!(
+ "{label}: {} unexpected trailing bytes",
+ self.remaining()
+ ));
+ }
+ Ok(())
+ }
+}
+
+fn read_sk(bytes: &[u8]) -> Result {
+ let arr: [u8; 32] = bytes
+ .try_into()
+ .map_err(|_| "secret key must be 32 bytes".to_string())?;
+ BBSplusSecretKey::from_bytes(&arr).map_err(err)
+}
+
+fn read_pk(bytes: &[u8]) -> Result {
+ let arr: [u8; 96] = bytes
+ .try_into()
+ .map_err(|_| "public key must be 96 bytes".to_string())?;
+ BBSplusPublicKey::from_bytes(&arr).map_err(err)
+}
+
+fn header_opt<'a>(bytes: &'a [u8]) -> Option<&'a [u8]> {
+ if bytes.is_empty() {
+ None
+ } else {
+ Some(bytes)
+ }
+}
+
+fn messages_opt<'a>(messages: &'a [Vec]) -> Option<&'a [Vec]> {
+ if messages.is_empty() {
+ None
+ } else {
+ Some(messages)
+ }
+}
+
+fn indexes_opt<'a>(idx: &'a [usize]) -> Option<&'a [usize]> {
+ if idx.is_empty() {
+ None
+ } else {
+ Some(idx)
+ }
+}
+
+// ---------- KeyGen --------------------------------------------------------
+
+fn op_keygen(input: &[u8]) -> Result, String> {
+ let (suite, rest) = read_suite(input)?;
+ with_suite!(suite, keygen_typed, rest)
+}
+
+fn keygen_typed(input: &[u8]) -> Result, String>
+where
+ CS: BbsCiphersuite,
+ CS::Expander: for<'a> ExpandMsg<'a>,
+{
+ let mut c = Cursor::new(input);
+ let key_material = c.var_bytes("key_material")?.to_vec();
+ let key_info_bytes = c.var_bytes("key_info")?.to_vec();
+ c.expect_eof("keygen request")?;
+ let key_info = header_opt(&key_info_bytes);
+ let kp = KeyPair::>::generate(&key_material, key_info, None).map_err(err)?;
+ Ok(kp.private_key().to_bytes().to_vec())
+}
+
+// ---------- SkToPk --------------------------------------------------------
+
+fn op_sk_to_pk(input: &[u8]) -> Result, String> {
+ let (suite, rest) = read_suite(input)?;
+ with_suite!(suite, sk_to_pk_typed, rest)
+}
+
+fn sk_to_pk_typed(input: &[u8]) -> Result, String>
+where
+ CS: BbsCiphersuite,
+{
+ let mut c = Cursor::new(input);
+ let sk = read_sk(c.take(32, "secret key")?)?;
+ c.expect_eof("sk_to_pk request")?;
+ let pk = sk.public_key();
+ Ok(pk.to_bytes().to_vec())
+}
+
+// ---------- Sign / Verify -------------------------------------------------
+
+fn op_sign(input: &[u8]) -> Result, String> {
+ let (suite, rest) = read_suite(input)?;
+ with_suite!(suite, sign_typed, rest)
+}
+
+fn sign_typed(input: &[u8]) -> Result, String>
+where
+ CS: BbsCiphersuite,
+ CS::Expander: for<'a> ExpandMsg<'a>,
+{
+ let mut c = Cursor::new(input);
+ let sk = read_sk(c.take(32, "secret key")?)?;
+ let pk = read_pk(c.take(96, "public key")?)?;
+ let header = c.var_bytes("header")?.to_vec();
+ let messages = c.message_list()?;
+ c.expect_eof("sign request")?;
+ let sig = Signature::>::sign(
+ messages_opt(&messages),
+ &sk,
+ &pk,
+ header_opt(&header),
+ )
+ .map_err(err)?;
+ Ok(sig.to_bytes().to_vec())
+}
+
+fn op_verify(input: &[u8]) -> Result, String> {
+ let (suite, rest) = read_suite(input)?;
+ with_suite!(suite, verify_typed, rest)
+}
+
+fn verify_typed(input: &[u8]) -> Result, String>
+where
+ CS: BbsCiphersuite,
+ CS::Expander: for<'a> ExpandMsg<'a>,
+{
+ let mut c = Cursor::new(input);
+ let pk = read_pk(c.take(96, "public key")?)?;
+ let sig_bytes = c.take(80, "signature")?.to_vec();
+ let header = c.var_bytes("header")?.to_vec();
+ let messages = c.message_list()?;
+ c.expect_eof("verify request")?;
+ let sig_arr: &[u8; 80] = sig_bytes
+ .as_slice()
+ .try_into()
+ .map_err(|_| "signature must be 80 bytes".to_string())?;
+ let sig = Signature::>::from_bytes(sig_arr).map_err(err)?;
+ let ok = sig
+ .verify(&pk, messages_opt(&messages), header_opt(&header))
+ .is_ok();
+ Ok(vec![if ok { 1 } else { 0 }])
+}
+
+// ---------- ProofGen / ProofVerify ----------------------------------------
+
+fn op_proof_gen(input: &[u8]) -> Result, String> {
+ let (suite, rest) = read_suite(input)?;
+ with_suite!(suite, proof_gen_typed, rest)
+}
+
+fn proof_gen_typed(input: &[u8]) -> Result, String>
+where
+ CS: BbsCiphersuite,
+ CS::Expander: for<'a> ExpandMsg<'a>,
+{
+ let mut c = Cursor::new(input);
+ let pk = read_pk(c.take(96, "public key")?)?;
+ let sig_bytes = c.take(80, "signature")?.to_vec();
+ let header = c.var_bytes("header")?.to_vec();
+ let ph = c.var_bytes("presentation header")?.to_vec();
+ let messages = c.message_list()?;
+ let disclosed = c.index_list()?;
+ c.expect_eof("proof_gen request")?;
+ let proof = PoKSignature::>::proof_gen(
+ &pk,
+ &sig_bytes,
+ header_opt(&header),
+ header_opt(&ph),
+ messages_opt(&messages),
+ indexes_opt(&disclosed),
+ )
+ .map_err(err)?;
+ Ok(proof.to_bytes())
+}
+
+fn op_proof_verify(input: &[u8]) -> Result, String> {
+ let (suite, rest) = read_suite(input)?;
+ with_suite!(suite, proof_verify_typed, rest)
+}
+
+fn proof_verify_typed(input: &[u8]) -> Result, String>
+where
+ CS: BbsCiphersuite,
+ CS::Expander: for<'a> ExpandMsg<'a>,
+{
+ let mut c = Cursor::new(input);
+ let pk = read_pk(c.take(96, "public key")?)?;
+ let proof_bytes = c.var_bytes("proof")?.to_vec();
+ let header = c.var_bytes("header")?.to_vec();
+ let ph = c.var_bytes("presentation header")?.to_vec();
+ let disclosed_messages = c.message_list()?;
+ let disclosed_indexes = c.index_list()?;
+ c.expect_eof("proof_verify request")?;
+ let proof = PoKSignature::>::from_bytes(&proof_bytes).map_err(err)?;
+ let ok = proof
+ .proof_verify(
+ &pk,
+ messages_opt(&disclosed_messages),
+ indexes_opt(&disclosed_indexes),
+ header_opt(&header),
+ header_opt(&ph),
+ )
+ .is_ok();
+ Ok(vec![if ok { 1 } else { 0 }])
+}
+
+// ---------- Error mapping -------------------------------------------------
+
+fn err(e: E) -> String {
+ format!("{e:?}")
+}
diff --git a/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmClient.java b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmClient.java
new file mode 100644
index 0000000..aea847e
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmClient.java
@@ -0,0 +1,518 @@
+package com.bloxbean.cardano.zeroj.bbs.wasm;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.dylibso.chicory.runtime.HostFunction;
+import com.dylibso.chicory.runtime.ImportValues;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.runtime.Memory;
+import com.dylibso.chicory.wasm.Parser;
+import com.dylibso.chicory.wasm.types.ValueType;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Chicory client for the ZeroJ CFRG BBS Rust WASM module.
+ *
+ *
Exposes the coarse {@code zeroj_bbs_*} ABI: keygen, sk_to_pk, sign, verify,
+ * proof_gen, proof_verify. The module imports exactly one host function,
+ * {@code env.zeroj_host_getrandom}, which this client wires to
+ * {@link SecureRandom}. See ADR-0019 §7.
+ */
+public final class Bbs12381WasmClient {
+ public static final String DEFAULT_RESOURCE = "/zeroj-bbs-wasm/zeroj_bbs.wasm";
+
+ private static final int MAX_RESPONSE_LEN = 16 * 1024 * 1024;
+ private static final int MAX_HOST_GETRANDOM_LEN = 16 * 1024;
+
+ /** Maximum number of signed messages or revealed indexes per request (matches the Rust cap). */
+ static final int MAX_MESSAGES = 1024;
+ /** Maximum bytes per individual message. */
+ static final int MAX_MESSAGE_BYTES = 64 * 1024;
+ /** Maximum bytes for header / presentation header. */
+ static final int MAX_HEADER_BYTES = 64 * 1024;
+ /** Maximum bytes for key material / key info. */
+ static final int MAX_KEY_INPUT_BYTES = 64 * 1024;
+ /** Maximum bytes for an opaque proof. */
+ static final int MAX_PROOF_BYTES = 3 * 96 + (4 + MAX_MESSAGES) * 32;
+ /** Maximum total serialized request size. */
+ static final int MAX_REQUEST_BYTES = 16 * 1024 * 1024;
+
+ static final byte SUITE_SHA256 = 0;
+ static final byte SUITE_SHAKE256 = 1;
+
+ private final byte[] wasmBytes;
+ private final Instance instance;
+ private final Memory memory;
+ private final SecureRandom defaultRandom;
+
+ public Bbs12381WasmClient(byte[] wasmBytes, SecureRandom random) {
+ Objects.requireNonNull(wasmBytes, "wasmBytes required");
+ Objects.requireNonNull(random, "random required");
+ if (wasmBytes.length == 0) {
+ throw new IllegalArgumentException("wasmBytes must not be empty");
+ }
+ this.wasmBytes = wasmBytes.clone();
+ this.defaultRandom = random;
+ try {
+ // Persistent instance for ops that consume no entropy (keygen,
+ // sk_to_pk, sign, verify, proof_verify) — bound to defaultRandom
+ // since they should never reach the host RNG anyway.
+ this.instance = buildInstance(this.wasmBytes, this.defaultRandom);
+ this.memory = Objects.requireNonNull(instance.memory(), "BBS WASM module must export memory");
+ long version = instance.export("zeroj_bbs_version").apply()[0];
+ if (version != 1L) {
+ throw new Bbs12381WasmException("Unsupported BBS WASM ABI version: " + version);
+ }
+ } catch (Bbs12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bbs12381WasmException("Failed to initialize BBS WASM module", e);
+ }
+ }
+
+ /**
+ * Build a fresh Chicory Instance whose {@code env.zeroj_host_getrandom}
+ * import draws from the given {@link SecureRandom}. A fresh instance is
+ * required for every {@code proof_gen} call because zkryptium's internal
+ * {@code rand::thread_rng()} keeps a cached chacha-state per thread that
+ * only re-seeds from {@code getrandom} on the first use — so without a
+ * fresh instance, calls 2..N silently bypass the per-call SecureRandom and
+ * derive bytes from the cached state seeded by call 1.
+ */
+ private static Instance buildInstance(byte[] wasmBytes, SecureRandom random) {
+ HostFunction hostGetrandom = new HostFunction(
+ "env",
+ "zeroj_host_getrandom",
+ List.of(ValueType.I32, ValueType.I32),
+ List.of(ValueType.I32),
+ (inst, args) -> {
+ int ptr = (int) args[0];
+ int len = (int) args[1];
+ if (len < 0 || len > MAX_HOST_GETRANDOM_LEN) {
+ return new long[]{1L};
+ }
+ if (len == 0) {
+ return new long[]{0L};
+ }
+ byte[] buf = new byte[len];
+ random.nextBytes(buf);
+ inst.memory().write(ptr, buf);
+ return new long[]{0L};
+ });
+ ImportValues imports = ImportValues.builder().addFunction(hostGetrandom).build();
+ return Instance.builder(Parser.parse(wasmBytes)).withImportValues(imports).build();
+ }
+
+ public static Bbs12381WasmClient fromPath(Path wasmPath) throws IOException {
+ return new Bbs12381WasmClient(Files.readAllBytes(wasmPath), new SecureRandom());
+ }
+
+ public static Bbs12381WasmClient createDefault() {
+ return createDefault(new SecureRandom());
+ }
+
+ public static Bbs12381WasmClient createDefault(SecureRandom random) {
+ try (var in = Bbs12381WasmClient.class.getResourceAsStream(DEFAULT_RESOURCE)) {
+ if (in == null) {
+ throw new Bbs12381WasmException("BBS WASM resource not found: " + DEFAULT_RESOURCE);
+ }
+ return new Bbs12381WasmClient(in.readAllBytes(), random);
+ } catch (IOException e) {
+ throw new Bbs12381WasmException("Failed to read BBS WASM resource", e);
+ }
+ }
+
+ // ----- typed entry points -----
+
+ public byte[] keyGen(BbsCiphersuite ciphersuite, byte[] keyMaterial, byte[] keyInfo) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ requireMaxLength(keyMaterial, MAX_KEY_INPUT_BYTES, "keyMaterial");
+ requireMaxLength(keyInfo, MAX_KEY_INPUT_BYTES, "keyInfo");
+ long size = 1L + 4 + keyMaterial.length + 4 + keyInfo.length;
+ ByteBuffer req = allocateRequest(size, "keyGen");
+ req.put(suite(ciphersuite));
+ putVarBytes(req, keyMaterial);
+ putVarBytes(req, keyInfo);
+ return invoke("zeroj_bbs_keygen", req.array());
+ }
+
+ public byte[] skToPk(BbsCiphersuite ciphersuite, byte[] sk) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(sk, "sk required");
+ if (sk.length != 32) {
+ throw new IllegalArgumentException("BBS secret key must be 32 bytes, got " + sk.length);
+ }
+ ByteBuffer req = ByteBuffer.allocate(1 + 32).order(ByteOrder.LITTLE_ENDIAN);
+ req.put(suite(ciphersuite));
+ req.put(sk);
+ return invoke("zeroj_bbs_sk_to_pk", req.array());
+ }
+
+ public byte[] sign(BbsCiphersuite ciphersuite, byte[] sk, byte[] pk, byte[] header, List messages) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(sk, "sk required");
+ if (sk.length != 32) {
+ throw new IllegalArgumentException("BBS secret key must be 32 bytes, got " + sk.length);
+ }
+ requireLength(pk, 96, "BBS public key");
+ requireMaxLength(header, MAX_HEADER_BYTES, "header");
+ long messagesBytes = validateMessageList(messages, "messages");
+ long size = 1L + 32 + 96 + 4 + header.length + 4 + messagesBytes;
+ ByteBuffer req = allocateRequest(size, "sign");
+ req.put(suite(ciphersuite));
+ req.put(sk);
+ req.put(pk);
+ putVarBytes(req, header);
+ putMessageList(req, messages);
+ return invoke("zeroj_bbs_sign", req.array());
+ }
+
+ public boolean verify(
+ BbsCiphersuite ciphersuite, byte[] pk, byte[] signature, byte[] header, List messages) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ requireLength(pk, 96, "BBS public key");
+ requireLength(signature, 80, "BBS signature");
+ requireMaxLength(header, MAX_HEADER_BYTES, "header");
+ long messagesBytes = validateMessageList(messages, "messages");
+ long size = 1L + 96 + 80 + 4 + header.length + 4 + messagesBytes;
+ ByteBuffer req = allocateRequest(size, "verify");
+ req.put(suite(ciphersuite));
+ req.put(pk);
+ req.put(signature);
+ putVarBytes(req, header);
+ putMessageList(req, messages);
+ byte[] response = invoke("zeroj_bbs_verify", req.array());
+ return decodeBool(response, "verify");
+ }
+
+ public byte[] proofGen(
+ BbsCiphersuite ciphersuite,
+ byte[] pk,
+ byte[] signature,
+ byte[] header,
+ byte[] presentationHeader,
+ List messages,
+ int[] disclosedIndexes,
+ SecureRandom random) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(random, "random required");
+ requireLength(pk, 96, "BBS public key");
+ requireLength(signature, 80, "BBS signature");
+ requireMaxLength(header, MAX_HEADER_BYTES, "header");
+ requireMaxLength(presentationHeader, MAX_HEADER_BYTES, "presentationHeader");
+ long messagesBytes = validateMessageList(messages, "messages");
+ validateIndexList(disclosedIndexes, messages.size(), "disclosedIndexes");
+ long size = 1L + 96 + 80 + 4 + header.length + 4 + presentationHeader.length
+ + 4 + messagesBytes + 4L + 4L * disclosedIndexes.length;
+ ByteBuffer req = allocateRequest(size, "proofGen");
+ req.put(suite(ciphersuite));
+ req.put(pk);
+ req.put(signature);
+ putVarBytes(req, header);
+ putVarBytes(req, presentationHeader);
+ putMessageList(req, messages);
+ req.putInt(disclosedIndexes.length);
+ for (int idx : disclosedIndexes) {
+ req.putInt(idx);
+ }
+ return invokeOnTransientInstance("zeroj_bbs_proof_gen", req.array(), random);
+ }
+
+ public boolean proofVerify(
+ BbsCiphersuite ciphersuite,
+ byte[] pk,
+ byte[] proof,
+ byte[] header,
+ byte[] presentationHeader,
+ List disclosedMessages,
+ int[] disclosedIndexes) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ requireLength(pk, 96, "BBS public key");
+ requireMaxLength(proof, MAX_PROOF_BYTES, "proof");
+ requireMaxLength(header, MAX_HEADER_BYTES, "header");
+ requireMaxLength(presentationHeader, MAX_HEADER_BYTES, "presentationHeader");
+ long messagesBytes = validateMessageList(disclosedMessages, "disclosedMessages");
+ validateDisclosedIndexesForVerify(disclosedIndexes, "disclosedIndexes");
+ if (disclosedIndexes.length != disclosedMessages.size()) {
+ throw new IllegalArgumentException(
+ "disclosedIndexes (" + disclosedIndexes.length
+ + ") and disclosedMessages (" + disclosedMessages.size() + ") must have equal length");
+ }
+ long size = 1L + 96 + 4 + proof.length + 4 + header.length + 4 + presentationHeader.length
+ + 4 + messagesBytes + 4L + 4L * disclosedIndexes.length;
+ ByteBuffer req = allocateRequest(size, "proofVerify");
+ req.put(suite(ciphersuite));
+ req.put(pk);
+ putVarBytes(req, proof);
+ putVarBytes(req, header);
+ putVarBytes(req, presentationHeader);
+ putMessageList(req, disclosedMessages);
+ req.putInt(disclosedIndexes.length);
+ for (int idx : disclosedIndexes) {
+ req.putInt(idx);
+ }
+ byte[] response = invoke("zeroj_bbs_proof_verify", req.array());
+ return decodeBool(response, "proof_verify");
+ }
+
+ // ----- test hooks -----
+
+ byte[] invokeRawForTesting(String exportName, byte[] request) {
+ return invoke(exportName, request);
+ }
+
+ byte[] invokeNoArgRawForTesting(String exportName) {
+ return invokeNoArg(exportName);
+ }
+
+ long invokeExportForTesting(String exportName, long... args) {
+ return instance.export(exportName).apply(args)[0];
+ }
+
+ // ----- internal -----
+
+ /**
+ * Run {@code exportName} on a freshly-built Chicory instance whose
+ * getrandom import draws from {@code random}. The instance is discarded
+ * after the call. See {@link #buildInstance} for the rationale: this is
+ * what guarantees the per-call SecureRandom drives every proof generation,
+ * not just the first one per shared instance.
+ */
+ private byte[] invokeOnTransientInstance(String exportName, byte[] request, SecureRandom random) {
+ Instance transient_ = buildInstance(wasmBytes, random);
+ Memory transientMemory = Objects.requireNonNull(
+ transient_.memory(), "BBS WASM module must export memory");
+ int requestPtr = 0;
+ int responsePtr = 0;
+ long responseAllocationLen = 0;
+ try {
+ requestPtr = (int) transient_.export("alloc").apply(request.length)[0];
+ transientMemory.write(requestPtr, request);
+ responsePtr = (int) transient_.export(exportName).apply(requestPtr, request.length)[0];
+ long responseLen = readResponseLenHeader(transientMemory, responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(transientMemory, exportName, responsePtr, (int) responseLen);
+ } catch (Bbs12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bbs12381WasmException("BBS WASM invocation failed: " + exportName, e);
+ } finally {
+ if (requestPtr != 0) {
+ transient_.export("dealloc").apply(requestPtr, request.length);
+ }
+ if (responsePtr != 0 && responseAllocationLen > 0) {
+ transient_.export("dealloc").apply(responsePtr, responseAllocationLen);
+ }
+ }
+ }
+
+ private synchronized byte[] invoke(String exportName, byte[] request) {
+ int requestPtr = 0;
+ int responsePtr = 0;
+ long responseAllocationLen = 0;
+ try {
+ requestPtr = (int) instance.export("alloc").apply(request.length)[0];
+ memory.write(requestPtr, request);
+ responsePtr = (int) instance.export(exportName).apply(requestPtr, request.length)[0];
+ long responseLen = readResponseLenHeader(memory, responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(memory, exportName, responsePtr, (int) responseLen);
+ } catch (Bbs12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bbs12381WasmException("BBS WASM invocation failed: " + exportName, e);
+ } finally {
+ if (requestPtr != 0) {
+ instance.export("dealloc").apply(requestPtr, request.length);
+ }
+ if (responsePtr != 0 && responseAllocationLen > 0) {
+ instance.export("dealloc").apply(responsePtr, responseAllocationLen);
+ }
+ }
+ }
+
+ private synchronized byte[] invokeNoArg(String exportName) {
+ int responsePtr = 0;
+ long responseAllocationLen = 0;
+ try {
+ responsePtr = (int) instance.export(exportName).apply()[0];
+ long responseLen = readResponseLenHeader(memory, responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(memory, exportName, responsePtr, (int) responseLen);
+ } catch (Bbs12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bbs12381WasmException("BBS WASM invocation failed: " + exportName, e);
+ } finally {
+ if (responsePtr != 0 && responseAllocationLen > 0) {
+ instance.export("dealloc").apply(responsePtr, responseAllocationLen);
+ }
+ }
+ }
+
+ private long readResponseLenHeader(Memory mem, int responsePtr) {
+ byte[] lenBytes = mem.readBytes(responsePtr, 4);
+ return Integer.toUnsignedLong(ByteBuffer.wrap(lenBytes).order(ByteOrder.LITTLE_ENDIAN).getInt());
+ }
+
+ private void requireValidResponseLen(long responseLen) {
+ if (responseLen == 0 || responseLen > MAX_RESPONSE_LEN) {
+ throw new Bbs12381WasmException("Invalid BBS WASM response length: " + responseLen);
+ }
+ }
+
+ private long responseAllocationLen(long responseLen) {
+ long maxWasmAllocationLen = Integer.toUnsignedLong(-1);
+ return responseLen <= maxWasmAllocationLen - 4 ? responseLen + 4 : 0;
+ }
+
+ private byte[] readResponsePayload(Memory mem, String exportName, int responsePtr, int responseLen) {
+ byte[] response = mem.readBytes(responsePtr + 4, responseLen);
+ if (response[0] != 0) {
+ String message = new String(response, 1, response.length - 1, StandardCharsets.UTF_8);
+ throw new Bbs12381WasmException("BBS WASM error from " + exportName + ": " + message);
+ }
+ return Arrays.copyOfRange(response, 1, response.length);
+ }
+
+ private static byte suite(BbsCiphersuite ciphersuite) {
+ return switch (ciphersuite) {
+ case BLS12381_SHA256 -> SUITE_SHA256;
+ case BLS12381_SHAKE256 -> SUITE_SHAKE256;
+ };
+ }
+
+ private static void putVarBytes(ByteBuffer buf, byte[] bytes) {
+ buf.putInt(bytes.length);
+ buf.put(bytes);
+ }
+
+ private static void putMessageList(ByteBuffer buf, List messages) {
+ buf.putInt(messages.size());
+ for (byte[] msg : messages) {
+ putVarBytes(buf, msg);
+ }
+ }
+
+ private static long validateMessageList(List messages, String label) {
+ Objects.requireNonNull(messages, label + " required");
+ if (messages.size() > MAX_MESSAGES) {
+ throw new IllegalArgumentException(
+ label + " count " + messages.size() + " exceeds " + MAX_MESSAGES);
+ }
+ long total = 0L;
+ for (int i = 0; i < messages.size(); i++) {
+ byte[] m = messages.get(i);
+ if (m == null) {
+ throw new IllegalArgumentException(label + "[" + i + "] must not be null");
+ }
+ if (m.length > MAX_MESSAGE_BYTES) {
+ throw new IllegalArgumentException(
+ label + "[" + i + "] length " + m.length + " exceeds " + MAX_MESSAGE_BYTES);
+ }
+ total = Math.addExact(total, 4L + m.length);
+ }
+ return total;
+ }
+
+ private static void validateIndexList(int[] indexes, int messageCount, String label) {
+ Objects.requireNonNull(indexes, label + " required");
+ if (indexes.length > MAX_MESSAGES) {
+ throw new IllegalArgumentException(
+ label + " count " + indexes.length + " exceeds " + MAX_MESSAGES);
+ }
+ int previous = -1;
+ for (int i = 0; i < indexes.length; i++) {
+ int idx = indexes[i];
+ if (idx < 0 || idx >= messageCount) {
+ throw new IllegalArgumentException(
+ label + "[" + i + "] = " + idx + " out of range [0, " + messageCount + ")");
+ }
+ if (idx <= previous) {
+ throw new IllegalArgumentException(
+ label + " must be strictly ascending; "
+ + label + "[" + i + "] = " + idx + " is not greater than previous " + previous);
+ }
+ previous = idx;
+ }
+ }
+
+ /**
+ * Like {@link #validateIndexList} but for the proof_verify path where the
+ * total message count is not known to the caller (only the disclosed
+ * subset is). Validates strict-ascending, no-duplicate, non-negative.
+ */
+ private static void validateDisclosedIndexesForVerify(int[] indexes, String label) {
+ Objects.requireNonNull(indexes, label + " required");
+ if (indexes.length > MAX_MESSAGES) {
+ throw new IllegalArgumentException(
+ label + " count " + indexes.length + " exceeds " + MAX_MESSAGES);
+ }
+ int previous = -1;
+ for (int i = 0; i < indexes.length; i++) {
+ int idx = indexes[i];
+ if (idx < 0) {
+ throw new IllegalArgumentException(
+ label + "[" + i + "] = " + idx + " must be non-negative");
+ }
+ if (idx <= previous) {
+ throw new IllegalArgumentException(
+ label + " must be strictly ascending; "
+ + label + "[" + i + "] = " + idx + " is not greater than previous " + previous);
+ }
+ previous = idx;
+ }
+ }
+
+ private static void requireLength(byte[] arr, int length, String label) {
+ Objects.requireNonNull(arr, label + " required");
+ if (arr.length != length) {
+ throw new IllegalArgumentException(label + " must be " + length + " bytes, got " + arr.length);
+ }
+ }
+
+ private static void requireMaxLength(byte[] arr, int max, String label) {
+ Objects.requireNonNull(arr, label + " required");
+ if (arr.length > max) {
+ throw new IllegalArgumentException(
+ label + " length " + arr.length + " exceeds " + max);
+ }
+ }
+
+ private static ByteBuffer allocateRequest(long size, String op) {
+ if (size > MAX_REQUEST_BYTES) {
+ throw new IllegalArgumentException(
+ "BBS WASM " + op + " request size " + size + " exceeds " + MAX_REQUEST_BYTES);
+ }
+ return ByteBuffer.allocate(Math.toIntExact(size)).order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ private static boolean decodeBool(byte[] response, String exportName) {
+ if (response.length != 1) {
+ throw new Bbs12381WasmException(
+ "Invalid BBS WASM " + exportName + " response length: " + response.length);
+ }
+ byte b = response[0];
+ if (b == 0) {
+ return false;
+ }
+ if (b == 1) {
+ return true;
+ }
+ throw new Bbs12381WasmException(
+ "Invalid BBS WASM " + exportName + " boolean response byte: 0x"
+ + String.format("%02x", b & 0xff));
+ }
+}
diff --git a/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmException.java b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmException.java
new file mode 100644
index 0000000..52a255d
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmException.java
@@ -0,0 +1,14 @@
+package com.bloxbean.cardano.zeroj.bbs.wasm;
+
+/**
+ * Runtime exception raised by the Chicory-backed CFRG BBS WASM provider.
+ */
+public class Bbs12381WasmException extends RuntimeException {
+ public Bbs12381WasmException(String message) {
+ super(message);
+ }
+
+ public Bbs12381WasmException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
new file mode 100644
index 0000000..93cd34d
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
@@ -0,0 +1,152 @@
+package com.bloxbean.cardano.zeroj.bbs.wasm;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bbs.BbsProof;
+import com.bloxbean.cardano.zeroj.bbs.BbsPublicKey;
+import com.bloxbean.cardano.zeroj.bbs.BbsSecretKey;
+import com.bloxbean.cardano.zeroj.bbs.BbsSignature;
+import com.bloxbean.cardano.zeroj.bbs.internal.BbsCodec;
+import com.bloxbean.cardano.zeroj.bbs.spi.BbsProvider;
+
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Full Rust-WASM CFRG BBS provider. The entire BBS algorithm runs inside
+ * WebAssembly (zkryptium 0.6.1 compiled to {@code wasm32-unknown-unknown})
+ * executed through Chicory. ZeroJ's Java layer only serializes requests,
+ * parses responses, and supplies entropy via the single named host import
+ * {@code env.zeroj_host_getrandom}.
+ *
+ *
This provider is per-{@link BbsCiphersuite}. Construct one instance per
+ * ciphersuite you intend to use. The underlying {@link Bbs12381WasmClient} is
+ * thread-safe; the wrapper does no further synchronization.
+ */
+public final class WasmBbsProvider implements BbsProvider {
+ private final BbsCiphersuite ciphersuite;
+ private final Bbs12381WasmClient client;
+
+ public WasmBbsProvider(BbsCiphersuite ciphersuite, Bbs12381WasmClient client) {
+ this.ciphersuite = Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ this.client = Objects.requireNonNull(client, "client required");
+ }
+
+ public static WasmBbsProvider createDefault() {
+ return createDefault(BbsCiphersuite.BLS12381_SHA256);
+ }
+
+ public static WasmBbsProvider createDefault(BbsCiphersuite ciphersuite) {
+ return new WasmBbsProvider(ciphersuite, Bbs12381WasmClient.createDefault());
+ }
+
+ public static WasmBbsProvider createDefault(BbsCiphersuite ciphersuite, SecureRandom random) {
+ return new WasmBbsProvider(ciphersuite, Bbs12381WasmClient.createDefault(random));
+ }
+
+ @Override
+ public String id() {
+ return "zeroj-bbs-wasm-zkryptium";
+ }
+
+ @Override
+ public BbsCiphersuite ciphersuite() {
+ return ciphersuite;
+ }
+
+ @Override
+ public BbsSecretKey keyGen(byte[] keyMaterial, byte[] keyInfo) {
+ byte[] sk = client.keyGen(ciphersuite, keyMaterial, keyInfo);
+ return new BbsSecretKey(BbsCodec.scalarFromBytes(sk, "BBS secret key"), ciphersuite);
+ }
+
+ @Override
+ public BbsPublicKey skToPk(BbsSecretKey secretKey) {
+ requireSuite(secretKey);
+ byte[] pk = client.skToPk(ciphersuite, secretKey.toBytes());
+ return new BbsPublicKey(pk, ciphersuite);
+ }
+
+ @Override
+ public BbsSignature sign(
+ BbsSecretKey secretKey, BbsPublicKey publicKey, List messages, byte[] header) {
+ requireSuite(secretKey);
+ requireSuite(publicKey);
+ byte[] sig = client.sign(ciphersuite, secretKey.toBytes(), publicKey.bytes(), header, messages);
+ return new BbsSignature(sig, ciphersuite);
+ }
+
+ @Override
+ public boolean verify(
+ BbsPublicKey publicKey, BbsSignature signature, List messages, byte[] header) {
+ if (publicKey.ciphersuite() != ciphersuite || signature.ciphersuite() != ciphersuite) {
+ return false;
+ }
+ return client.verify(ciphersuite, publicKey.bytes(), signature.bytes(), header, messages);
+ }
+
+ @Override
+ public BbsProof proofGen(
+ BbsPublicKey publicKey,
+ BbsSignature signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes,
+ SecureRandom random) {
+ // Per-call SecureRandom drives the host getrandom import for the
+ // duration of this synchronized invocation, matching the contract
+ // honored by PureJavaBbsProvider.
+ requireSuite(publicKey);
+ requireSuite(signature);
+ byte[] proof = client.proofGen(
+ ciphersuite,
+ publicKey.bytes(),
+ signature.bytes(),
+ header,
+ presentationHeader,
+ messages,
+ disclosedIndexes,
+ Objects.requireNonNull(random, "random required"));
+ return new BbsProof(proof, ciphersuite);
+ }
+
+ @Override
+ public boolean proofVerify(
+ BbsPublicKey publicKey,
+ BbsProof proof,
+ byte[] header,
+ byte[] presentationHeader,
+ List disclosedMessages,
+ int[] disclosedIndexes) {
+ if (publicKey.ciphersuite() != ciphersuite || proof.ciphersuite() != ciphersuite) {
+ return false;
+ }
+ return client.proofVerify(
+ ciphersuite,
+ publicKey.bytes(),
+ proof.bytes(),
+ header,
+ presentationHeader,
+ disclosedMessages,
+ disclosedIndexes);
+ }
+
+ private void requireSuite(BbsSecretKey secretKey) {
+ if (Objects.requireNonNull(secretKey, "secret key required").ciphersuite() != ciphersuite) {
+ throw new IllegalArgumentException("BBS secret key ciphersuite mismatch");
+ }
+ }
+
+ private void requireSuite(BbsPublicKey publicKey) {
+ if (Objects.requireNonNull(publicKey, "public key required").ciphersuite() != ciphersuite) {
+ throw new IllegalArgumentException("BBS public key ciphersuite mismatch");
+ }
+ }
+
+ private void requireSuite(BbsSignature signature) {
+ if (Objects.requireNonNull(signature, "signature required").ciphersuite() != ciphersuite) {
+ throw new IllegalArgumentException("BBS signature ciphersuite mismatch");
+ }
+ }
+}
diff --git a/zeroj-bbs-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bbs-wasm/resource-config.json b/zeroj-bbs-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bbs-wasm/resource-config.json
new file mode 100644
index 0000000..6dac94f
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bbs-wasm/resource-config.json
@@ -0,0 +1,9 @@
+{
+ "resources": {
+ "includes": [
+ {
+ "pattern": "\\Qzeroj-bbs-wasm/zeroj_bbs.wasm\\E"
+ }
+ ]
+ }
+}
diff --git a/zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java b/zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
new file mode 100644
index 0000000..72d5d50
--- /dev/null
+++ b/zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
@@ -0,0 +1,684 @@
+package com.bloxbean.cardano.zeroj.bbs.wasm;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bbs.BbsKeyPair;
+import com.bloxbean.cardano.zeroj.bbs.BbsProof;
+import com.bloxbean.cardano.zeroj.bbs.BbsPublicKey;
+import com.bloxbean.cardano.zeroj.bbs.BbsSecretKey;
+import com.bloxbean.cardano.zeroj.bbs.BbsSignature;
+import com.dylibso.chicory.wasm.Parser;
+import com.dylibso.chicory.wasm.types.ExternalType;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WasmBbsProviderTest {
+
+ private static final byte[] KEY_MATERIAL = hex("746869732d49532d6a7573742d616e2d546573742d494b4d2d746f2d67656e65726174652d246528724074232d6b6579");
+ private static final byte[] KEY_INFO = hex("746869732d49532d736f6d652d6b65792d6d657461646174612d746f2d62652d757365642d696e2d746573742d6b65792d67656e");
+ private static final byte[] EXPECTED_SK = hex("60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc");
+ private static final byte[] EXPECTED_PK = hex("a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c");
+ private static final byte[] HEADER = hex("11223344556677889900aabbccddeeff");
+ private static final byte[] PRESENTATION_HEADER = hex("bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501");
+ private static final byte[] SINGLE_MSG = hex("9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02");
+ private static final byte[] EXPECTED_SIG_SHA256 = hex("84773160b824e194073a57493dac1a20b667af70cd2352d8af241c77658da5253aa8458317cca0eae615690d55b1f27164657dcafee1d5c1973947aa70e2cfbb4c892340be5969920d0916067b4565a0");
+
+ // SHAKE-256 ciphersuite fixtures (same key_material + key_info, different ciphersuite).
+ private static final byte[] EXPECTED_SK_SHAKE = hex("2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079");
+ private static final byte[] EXPECTED_PK_SHAKE = hex("92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5");
+ private static final byte[] EXPECTED_SIG_SHAKE_SINGLE = hex("b9a622a4b404e6ca4c85c15739d2124a1deb16df750be202e2430e169bc27fb71c44d98e6d40792033e1c452145ada95030832c5dc778334f2f1b528eced21b0b97a12025a283d78b7136bb9825d04ef");
+ private static final byte[] EXPECTED_SIG_SHAKE_MULTI = hex("956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e");
+
+ // SHA-256 multi-message signature (signature004.json, 10 messages, with header).
+ private static final byte[] EXPECTED_SIG_SHA256_MULTI = hex("8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8");
+
+ // SHA-256 no-header signature (signature010.json, 10 messages, empty header).
+ private static final byte[] EXPECTED_SIG_SHA256_NOHEADER = hex("8c87e2080859a97299c148427cd2fcf390d24bea850103a9748879039262ecf4f42206f6ef767f298b6a96b424c1e86c26f8fba62212d0e05b95261c2cc0e5fdc63a32731347e810fd12e9c58355aa0d");
+
+ private static List tenFixtureMessages() {
+ return List.of(
+ hex("9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"),
+ hex("c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"),
+ hex("7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73"),
+ hex("77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c"),
+ hex("496694774c5604ab1b2544eababcf0f53278ff50"),
+ hex("515ae153e22aae04ad16f759e07237b4"),
+ hex("d183ddc6e2665aa4e2f088af"),
+ hex("ac55fb33a75909ed"),
+ hex("96012096"),
+ new byte[0]);
+ }
+
+ // SHA-256 proof001: single-message revealed proof; CFRG mockedRng-derived
+ // proof bytes; proof_verify must accept.
+ private static final byte[] PROOF_SHA256_PROOF001 = hex("94916292a7a6bade28456c601d3af33fcf39278d6594b467e128a3f83686a104ef2b2fcf72df0215eeaf69262ffe8194a19fab31a82ddbe06908985abc4c9825788b8a1610942d12b7f5debbea8985296361206dbace7af0cc834c80f33e0aadaeea5597befbb651827b5eed5a66f1a959bb46cfd5ca1a817a14475960f69b32c54db7587b5ee3ab665fbd37b506830a49f21d592f5e634f47cee05a025a2f8f94e73a6c15f02301d1178a92873b6e8634bafe4983c3e15a663d64080678dbf29417519b78af042be2b3e1c4d08b8d520ffab008cbaaca5671a15b22c239b38e940cfeaa5e72104576a9ec4a6fad78c532381aeaa6fb56409cef56ee5c140d455feeb04426193c57086c9b6d397d9418");
+
+ // SHAKE-256 proof001.
+ private static final byte[] PROOF_SHAKE_PROOF001 = hex("89e4ab0c160880e0c2f12a754b9c051ed7f5fccfee3d5cbbb62e1239709196c737fff4303054660f8fcd08267a5de668a2e395ebe8866bdcb0dff9786d7014fa5e3c8cf7b41f8d7510e27d307f18032f6b788e200b9d6509f40ce1d2f962ceedb023d58ee44d660434e6ba60ed0da1a5d2cde031b483684cd7c5b13295a82f57e209b584e8fe894bcc964117bf3521b43d8e2eb59ce31f34d68b39f05bb2c625e4de5e61e95ff38bfd62ab07105d016414b45b01625c69965ad3c8a933e7b25d93daeb777302b966079827a99178240e6c3f13b7db2fb1f14790940e239d775ab32f539bdf9f9b582b250b05882996832652f7f5d3b6e04744c73ada1702d6791940ccbd75e719537f7ace6ee817298d");
+
+ // SHA-256 proof003: 10-message signature, disclosed {0, 2, 4, 6}, 6 hidden.
+ // Exercises the core BBS selective-disclosure path against an official
+ // CFRG mockedRng-derived proof.
+ private static final byte[] PROOF_SHA256_PROOF003 = hex("a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a");
+
+ @Test
+ void wasmModule_hasExactlyOneImportAndExpectedExports() throws IOException {
+ var module = Parser.parse(loadDefaultWasm());
+
+ assertEquals(1, module.importSection().importCount());
+ var imp = module.importSection().getImport(0);
+ assertEquals("env", imp.module());
+ assertEquals("zeroj_host_getrandom", imp.name());
+ assertEquals(ExternalType.FUNCTION, imp.importType());
+
+ Set exports = new HashSet<>();
+ for (int i = 0; i < module.exportSection().exportCount(); i++) {
+ var export = module.exportSection().getExport(i);
+ if (export.exportType() == ExternalType.FUNCTION) {
+ exports.add(export.name());
+ }
+ }
+ assertTrue(exports.contains("zeroj_bbs_version"));
+ assertTrue(exports.contains("zeroj_bbs_keygen"));
+ assertTrue(exports.contains("zeroj_bbs_sk_to_pk"));
+ assertTrue(exports.contains("zeroj_bbs_sign"));
+ assertTrue(exports.contains("zeroj_bbs_verify"));
+ assertTrue(exports.contains("zeroj_bbs_proof_gen"));
+ assertTrue(exports.contains("zeroj_bbs_proof_verify"));
+ assertTrue(exports.contains("alloc"));
+ assertTrue(exports.contains("dealloc"));
+ }
+
+ @Test
+ void keygenAndSkToPk_matchDraft10ShaFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+
+ BbsSecretKey sk = provider.keyGen(KEY_MATERIAL, KEY_INFO);
+ assertArrayEquals(EXPECTED_SK, sk.toBytes());
+
+ BbsPublicKey pk = provider.skToPk(sk);
+ assertArrayEquals(EXPECTED_PK, pk.bytes());
+ }
+
+ @Test
+ void sign_matchesDraft10ShaSingleMessageFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), List.of(SINGLE_MSG), HEADER);
+
+ assertArrayEquals(EXPECTED_SIG_SHA256, sig.bytes());
+ assertTrue(provider.verify(kp.publicKey(), sig, List.of(SINGLE_MSG), HEADER));
+ }
+
+ @Test
+ void verify_rejectsTamperedSignature() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), List.of(SINGLE_MSG), HEADER);
+
+ byte[] bad = sig.bytes();
+ bad[bad.length - 1] ^= 1;
+
+ assertFalse(provider.verify(
+ kp.publicKey(),
+ new BbsSignature(bad, BbsCiphersuite.BLS12381_SHA256),
+ List.of(SINGLE_MSG),
+ HEADER));
+ }
+
+ @Test
+ void proofGen_roundtripsViaProofVerify() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = List.of(SINGLE_MSG);
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
+
+ BbsProof proof = provider.proofGen(
+ kp.publicKey(),
+ sig,
+ messages,
+ HEADER,
+ PRESENTATION_HEADER,
+ new int[]{0},
+ new SecureRandom());
+
+ assertTrue(provider.proofVerify(
+ kp.publicKey(),
+ proof,
+ HEADER,
+ PRESENTATION_HEADER,
+ messages,
+ new int[]{0}));
+ }
+
+ @Test
+ void proofGen_honorsPerCallSecureRandomOnEveryInvocation() {
+ // zkryptium's internal rand::thread_rng() seeds itself from getrandom
+ // exactly once per thread and then derives bytes from a cached chacha
+ // state. To honor ADR-0019 §7's per-call SecureRandom contract on
+ // every invocation, Bbs12381WasmClient.proofGen builds a *fresh*
+ // Chicory instance per call so ThreadRng has to re-seed each time.
+ //
+ // This test asserts the contract holds across N successive calls, not
+ // just the first one (which was a real Codex finding against the
+ // earlier shared-instance design).
+ var defaultCounter = new CountingSecureRandom();
+ var provider = new WasmBbsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ Bbs12381WasmClient.createDefault(defaultCounter));
+
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = List.of(SINGLE_MSG);
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
+
+ // Deterministic ops must NOT touch the host RNG.
+ assertEquals(0, defaultCounter.bytesRead, "deterministic ops must not call host getrandom");
+
+ int callCount = 5;
+ for (int i = 0; i < callCount; i++) {
+ var perCall = new CountingSecureRandom();
+ BbsProof proof = provider.proofGen(
+ kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER,
+ new int[]{0}, perCall);
+ assertTrue(perCall.bytesRead > 0,
+ "per-call SecureRandom must drive host getrandom on call " + i
+ + " (read " + perCall.bytesRead + " bytes)");
+ assertTrue(provider.proofVerify(
+ kp.publicKey(), proof, HEADER, PRESENTATION_HEADER, messages, new int[]{0}));
+ }
+ assertEquals(0, defaultCounter.bytesRead,
+ "defaultRandom must NOT be read across "
+ + callCount + " proofGen calls with per-call SecureRandoms");
+ }
+
+ /** SecureRandom subclass that records how many bytes have been consumed via nextBytes. */
+ private static final class CountingSecureRandom extends SecureRandom {
+ volatile int bytesRead = 0;
+
+ CountingSecureRandom() {
+ super(new java.security.SecureRandomSpi() {
+ @Override protected void engineSetSeed(byte[] seed) {}
+ @Override protected void engineNextBytes(byte[] bytes) {
+ // Fill with arbitrary deterministic bytes so proof_gen still
+ // succeeds (zkryptium will reject out-of-range scalars,
+ // 0x42... is well below r).
+ java.util.Arrays.fill(bytes, (byte) 0x42);
+ }
+ @Override protected byte[] engineGenerateSeed(int numBytes) {
+ byte[] b = new byte[numBytes];
+ java.util.Arrays.fill(b, (byte) 0x42);
+ return b;
+ }
+ }, null);
+ }
+
+ @Override
+ public void nextBytes(byte[] bytes) {
+ super.nextBytes(bytes);
+ bytesRead += bytes.length;
+ }
+ }
+
+ @Test
+ void proofGen_isNonDeterministicAcrossCalls() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = List.of(SINGLE_MSG);
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
+
+ BbsProof first = provider.proofGen(
+ kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER, new int[]{0}, new SecureRandom());
+ BbsProof second = provider.proofGen(
+ kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER, new int[]{0}, new SecureRandom());
+
+ assertFalse(java.util.Arrays.equals(first.bytes(), second.bytes()),
+ "host RNG must produce distinct proofs across calls");
+ assertTrue(provider.proofVerify(
+ kp.publicKey(), first, HEADER, PRESENTATION_HEADER, messages, new int[]{0}));
+ assertTrue(provider.proofVerify(
+ kp.publicKey(), second, HEADER, PRESENTATION_HEADER, messages, new int[]{0}));
+ }
+
+ @Test
+ void shake256_signRoundtripsViaVerify() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHAKE256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), List.of(SINGLE_MSG), HEADER);
+ assertTrue(provider.verify(kp.publicKey(), sig, List.of(SINGLE_MSG), HEADER));
+ }
+
+ @Test
+ void keygenAndSkToPk_matchDraft10ShakeFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHAKE256);
+
+ BbsSecretKey sk = provider.keyGen(KEY_MATERIAL, KEY_INFO);
+ assertArrayEquals(EXPECTED_SK_SHAKE, sk.toBytes());
+
+ BbsPublicKey pk = provider.skToPk(sk);
+ assertArrayEquals(EXPECTED_PK_SHAKE, pk.bytes());
+ }
+
+ @Test
+ void sign_matchesDraft10ShakeSingleMessageFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHAKE256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), List.of(SINGLE_MSG), HEADER);
+
+ assertArrayEquals(EXPECTED_SIG_SHAKE_SINGLE, sig.bytes());
+ assertTrue(provider.verify(kp.publicKey(), sig, List.of(SINGLE_MSG), HEADER));
+ }
+
+ @Test
+ void sign_matchesDraft10ShakeMultiMessageFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHAKE256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = tenFixtureMessages();
+
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
+
+ assertArrayEquals(EXPECTED_SIG_SHAKE_MULTI, sig.bytes());
+ assertTrue(provider.verify(kp.publicKey(), sig, messages, HEADER));
+ }
+
+ @Test
+ void sign_matchesDraft10ShaMultiMessageFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = tenFixtureMessages();
+
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
+
+ assertArrayEquals(EXPECTED_SIG_SHA256_MULTI, sig.bytes());
+ assertTrue(provider.verify(kp.publicKey(), sig, messages, HEADER));
+ }
+
+ @Test
+ void sign_matchesDraft10ShaNoHeaderFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = tenFixtureMessages();
+ byte[] emptyHeader = new byte[0];
+
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, emptyHeader);
+
+ assertArrayEquals(EXPECTED_SIG_SHA256_NOHEADER, sig.bytes());
+ assertTrue(provider.verify(kp.publicKey(), sig, messages, emptyHeader));
+ }
+
+ @Test
+ void proofVerify_acceptsOfficialDraft10ShaProofFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsPublicKey pk = new BbsPublicKey(EXPECTED_PK, BbsCiphersuite.BLS12381_SHA256);
+ BbsProof proof = new BbsProof(PROOF_SHA256_PROOF001, BbsCiphersuite.BLS12381_SHA256);
+ List disclosedMessages = List.of(SINGLE_MSG);
+ int[] disclosedIndexes = {0};
+
+ assertTrue(provider.proofVerify(
+ pk, proof, HEADER, PRESENTATION_HEADER, disclosedMessages, disclosedIndexes));
+ }
+
+ @Test
+ void proofVerify_acceptsOfficialDraft10ShakeProofFixture() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHAKE256);
+ BbsPublicKey pk = new BbsPublicKey(EXPECTED_PK_SHAKE, BbsCiphersuite.BLS12381_SHAKE256);
+ BbsProof proof = new BbsProof(PROOF_SHAKE_PROOF001, BbsCiphersuite.BLS12381_SHAKE256);
+ List disclosedMessages = List.of(SINGLE_MSG);
+ int[] disclosedIndexes = {0};
+
+ assertTrue(provider.proofVerify(
+ pk, proof, HEADER, PRESENTATION_HEADER, disclosedMessages, disclosedIndexes));
+ }
+
+ @Test
+ void proofGen_hiddenMessageSelectiveDisclosureRoundtrip() {
+ // Selective disclosure: 10 messages, reveal {0, 2, 4, 6} = 4 disclosed,
+ // 6 hidden. Exercises the core BBS selective-disclosure path through
+ // the WASM provider end-to-end.
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List allMessages = tenFixtureMessages();
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), allMessages, HEADER);
+
+ int[] disclosed = {0, 2, 4, 6};
+ BbsProof proof = provider.proofGen(
+ kp.publicKey(), sig, allMessages, HEADER, PRESENTATION_HEADER,
+ disclosed, new SecureRandom());
+
+ List disclosedMessages = List.of(
+ allMessages.get(0), allMessages.get(2), allMessages.get(4), allMessages.get(6));
+ assertTrue(provider.proofVerify(
+ kp.publicKey(), proof, HEADER, PRESENTATION_HEADER, disclosedMessages, disclosed));
+
+ // Same proof with disclosed messages from a different signature must reject.
+ List mangled = List.of(
+ allMessages.get(1), allMessages.get(2), allMessages.get(4), allMessages.get(6));
+ assertFalse(provider.proofVerify(
+ kp.publicKey(), proof, HEADER, PRESENTATION_HEADER, mangled, disclosed));
+ }
+
+ @Test
+ void proofVerify_acceptsOfficialDraft10ShaHiddenMessageProof() {
+ // proof003.json: 10-message signature with 4 revealed (disclosedIndexes
+ // [0, 2, 4, 6]) and 6 hidden. The official CFRG mockedRng-derived
+ // proof bytes; proof_verify must accept.
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsPublicKey pk = new BbsPublicKey(EXPECTED_PK, BbsCiphersuite.BLS12381_SHA256);
+ BbsProof proof = new BbsProof(PROOF_SHA256_PROOF003, BbsCiphersuite.BLS12381_SHA256);
+ List allMessages = tenFixtureMessages();
+ int[] disclosed = {0, 2, 4, 6};
+ List disclosedMessages = List.of(
+ allMessages.get(0), allMessages.get(2), allMessages.get(4), allMessages.get(6));
+
+ assertTrue(provider.proofVerify(
+ pk, proof, HEADER, PRESENTATION_HEADER, disclosedMessages, disclosed));
+ }
+
+ @Test
+ void proofGen_rejectsDuplicateDisclosedIndexes() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsKeyPair kp = provider.keyPair(KEY_MATERIAL, KEY_INFO);
+ List messages = tenFixtureMessages();
+ BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
+
+ IllegalArgumentException dup = assertThrows(IllegalArgumentException.class, () ->
+ provider.proofGen(
+ kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER,
+ new int[]{0, 0}, new SecureRandom()));
+ assertTrue(dup.getMessage().contains("strictly ascending"), dup.getMessage());
+
+ IllegalArgumentException unsorted = assertThrows(IllegalArgumentException.class, () ->
+ provider.proofGen(
+ kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER,
+ new int[]{2, 0}, new SecureRandom()));
+ assertTrue(unsorted.getMessage().contains("strictly ascending"), unsorted.getMessage());
+ }
+
+ @Test
+ void proofVerify_rejectsDuplicateDisclosedIndexes() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsPublicKey pk = new BbsPublicKey(EXPECTED_PK, BbsCiphersuite.BLS12381_SHA256);
+ BbsProof proof = new BbsProof(PROOF_SHA256_PROOF001, BbsCiphersuite.BLS12381_SHA256);
+ List dup = List.of(SINGLE_MSG, SINGLE_MSG);
+
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
+ provider.proofVerify(
+ pk, proof, HEADER, PRESENTATION_HEADER, dup, new int[]{0, 0}));
+ assertTrue(ex.getMessage().contains("strictly ascending"), ex.getMessage());
+ }
+
+ @Test
+ void proofVerify_rejectsOfficialProofWithWrongPresentationHeader() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ BbsPublicKey pk = new BbsPublicKey(EXPECTED_PK, BbsCiphersuite.BLS12381_SHA256);
+ BbsProof proof = new BbsProof(PROOF_SHA256_PROOF001, BbsCiphersuite.BLS12381_SHA256);
+ List disclosedMessages = List.of(SINGLE_MSG);
+ int[] disclosedIndexes = {0};
+ byte[] wrongPh = new byte[PRESENTATION_HEADER.length];
+ System.arraycopy(PRESENTATION_HEADER, 0, wrongPh, 0, PRESENTATION_HEADER.length);
+ wrongPh[0] ^= 1;
+
+ assertFalse(provider.proofVerify(
+ pk, proof, HEADER, wrongPh, disclosedMessages, disclosedIndexes));
+ }
+
+ @Test
+ void rawInvocation_reportsTypedExceptionOnShortInput() {
+ var client = Bbs12381WasmClient.createDefault();
+
+ assertThrows(Bbs12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bbs_sign", new byte[1]));
+ assertThrows(Bbs12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bbs_verify", new byte[]{}));
+ }
+
+ @Test
+ void rawInvocation_reportsTypedExceptionOnInvalidPublicKey() {
+ var client = Bbs12381WasmClient.createDefault();
+
+ byte[] req = new byte[1 + 32];
+ req[0] = Bbs12381WasmClient.SUITE_SHA256;
+ assertThrows(Bbs12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bbs_sk_to_pk",
+ java.util.Arrays.copyOf(req, req.length - 1)));
+ }
+
+ @Test
+ void repeatedErrors_doNotPoisonClient() {
+ var provider = WasmBbsProvider.createDefault(BbsCiphersuite.BLS12381_SHA256);
+ for (int i = 0; i < 50; i++) {
+ assertThrows(Bbs12381WasmException.class,
+ () -> provider.keyGen(new byte[0], new byte[0]));
+ }
+ BbsSecretKey sk = provider.keyGen(KEY_MATERIAL, KEY_INFO);
+ assertArrayEquals(EXPECTED_SK, sk.toBytes());
+ }
+
+ @Test
+ void malformedResponseLength_freesResponseAllocationOnInvoke() {
+ SecureRandom rng = new SecureRandom();
+ var malformed = new Bbs12381WasmClient(malformedResponseWasm(), rng);
+
+ assertThrows(Bbs12381WasmException.class,
+ () -> malformed.invokeRawForTesting("malformed_response", new byte[]{1, 2, 3}));
+
+ assertEquals(2, malformed.invokeExportForTesting("dealloc_count"));
+ assertEquals(4, malformed.invokeExportForTesting("last_dealloc_len"));
+ }
+
+ @Test
+ void malformedResponseLength_freesResponseAllocationOnInvokeNoArg() {
+ SecureRandom rng = new SecureRandom();
+ var malformed = new Bbs12381WasmClient(malformedResponseWasm(), rng);
+
+ assertThrows(Bbs12381WasmException.class,
+ () -> malformed.invokeNoArgRawForTesting("malformed_noarg"));
+
+ assertEquals(1, malformed.invokeExportForTesting("dealloc_count"));
+ assertEquals(4, malformed.invokeExportForTesting("last_dealloc_len"));
+ }
+
+ @Test
+ void verify_rejectsNonCanonicalBooleanResponse() {
+ SecureRandom rng = new SecureRandom();
+ var bad = new Bbs12381WasmClient(badBoolResponseWasm(), rng);
+
+ byte[] pk = new byte[96];
+ byte[] sig = new byte[80];
+ Bbs12381WasmException ex = assertThrows(
+ Bbs12381WasmException.class,
+ () -> bad.verify(BbsCiphersuite.BLS12381_SHA256, pk, sig, new byte[0], List.of()));
+ assertTrue(ex.getMessage().contains("boolean response byte"),
+ "expected strict bool decode error, got: " + ex.getMessage());
+ }
+
+ private static byte[] loadDefaultWasm() throws IOException {
+ try (var in = Bbs12381WasmClient.class.getResourceAsStream(Bbs12381WasmClient.DEFAULT_RESOURCE)) {
+ assertNotNull(in, "BBS WASM resource must be present on the classpath");
+ return in.readAllBytes();
+ }
+ }
+
+ // Hand-built synthetic WASM module exporting only the version-1 ABI shape
+ // we need to exercise the response-buffer cleanup path. Mirrors the
+ // technique in Bls12381WasmClientTest.malformedResponseWasm. Notably this
+ // synthetic module declares no imports, so it remains compatible with the
+ // Bbs12381WasmClient constructor (which always supplies the
+ // env.zeroj_host_getrandom import — Chicory ignores unused-import slots).
+ private static byte[] malformedResponseWasm() {
+ var wasm = new ByteArrayOutputStream();
+ write(wasm, 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00);
+ section(wasm, 1, out -> {
+ u32(out, 4);
+ funcType(out, 0, true);
+ funcType(out, 1, true);
+ funcType(out, 2, false);
+ funcType(out, 2, true);
+ });
+ section(wasm, 3, out -> {
+ u32(out, 7);
+ write(out, 0, 1, 2, 3, 0, 0, 0);
+ });
+ section(wasm, 5, out -> write(out, 1, 0, 1));
+ section(wasm, 7, out -> {
+ u32(out, 8);
+ export(out, "memory", 2, 0);
+ export(out, "zeroj_bbs_version", 0, 0);
+ export(out, "alloc", 0, 1);
+ export(out, "dealloc", 0, 2);
+ export(out, "malformed_response", 0, 3);
+ export(out, "malformed_noarg", 0, 4);
+ export(out, "dealloc_count", 0, 5);
+ export(out, "last_dealloc_len", 0, 6);
+ });
+ section(wasm, 10, out -> {
+ u32(out, 7);
+ // zeroj_bbs_version: return 1
+ code(out, 0x00, 0x41, 0x01, 0x0b);
+ // alloc: return 0x400 (1024)
+ code(out, 0x00, 0x41, 0x80, 0x08, 0x0b);
+ // dealloc: count++, last_len = arg1
+ code(out,
+ 0x00,
+ 0x41, 0x00,
+ 0x41, 0x00,
+ 0x28, 0x02, 0x00,
+ 0x41, 0x01,
+ 0x6a,
+ 0x36, 0x02, 0x00,
+ 0x41, 0x04,
+ 0x20, 0x01,
+ 0x36, 0x02, 0x00,
+ 0x0b);
+ // malformed_response: write 0 at addr 8, return 8
+ code(out, 0x00, 0x41, 0x08, 0x41, 0x00, 0x36, 0x02, 0x00, 0x41, 0x08, 0x0b);
+ // malformed_noarg: same
+ code(out, 0x00, 0x41, 0x08, 0x41, 0x00, 0x36, 0x02, 0x00, 0x41, 0x08, 0x0b);
+ // dealloc_count: load addr 0
+ code(out, 0x00, 0x41, 0x00, 0x28, 0x02, 0x00, 0x0b);
+ // last_dealloc_len: load addr 4
+ code(out, 0x00, 0x41, 0x04, 0x28, 0x02, 0x00, 0x0b);
+ });
+ return wasm.toByteArray();
+ }
+
+ // Synthetic WASM that always returns a "success" response with a bool
+ // payload byte of 0x02 (instead of the legal 0x00 or 0x01). Used to verify
+ // that Bbs12381WasmClient.decodeBool rejects non-canonical truthy values.
+ private static byte[] badBoolResponseWasm() {
+ var wasm = new ByteArrayOutputStream();
+ write(wasm, 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00);
+ section(wasm, 1, out -> {
+ u32(out, 4);
+ funcType(out, 0, true); // () -> i32
+ funcType(out, 1, true); // (i32) -> i32
+ funcType(out, 2, false); // (i32, i32) -> void
+ funcType(out, 2, true); // (i32, i32) -> i32
+ });
+ section(wasm, 3, out -> {
+ u32(out, 4);
+ write(out, 0, 1, 2, 3);
+ });
+ section(wasm, 5, out -> write(out, 1, 0, 1));
+ section(wasm, 7, out -> {
+ u32(out, 5);
+ export(out, "memory", 2, 0);
+ export(out, "zeroj_bbs_version", 0, 0);
+ export(out, "alloc", 0, 1);
+ export(out, "dealloc", 0, 2);
+ export(out, "zeroj_bbs_verify", 0, 3);
+ });
+ section(wasm, 10, out -> {
+ u32(out, 4);
+ // zeroj_bbs_version -> 1
+ code(out, 0x00, 0x41, 0x01, 0x0b);
+ // alloc -> 0x400 (1024)
+ code(out, 0x00, 0x41, 0x80, 0x08, 0x0b);
+ // dealloc -> no-op
+ code(out, 0x00, 0x0b);
+ // zeroj_bbs_verify: write [u32 LE 2 | 0x00 | 0x02] at addr 8, return 8.
+ // addr 8..12: response length = 2
+ // addr 12: status = 0 (success)
+ // addr 13: payload byte = 0x02 (illegal bool)
+ code(out,
+ 0x00,
+ 0x41, 0x08, 0x41, 0x02, 0x36, 0x02, 0x00, // i32.store [8] = 2
+ 0x41, 0x0c, 0x41, 0x00, 0x3a, 0x00, 0x00, // i32.store8 [12] = 0
+ 0x41, 0x0d, 0x41, 0x02, 0x3a, 0x00, 0x00, // i32.store8 [13] = 2
+ 0x41, 0x08, 0x0b); // return 8
+ });
+ return wasm.toByteArray();
+ }
+
+ private static void funcType(ByteArrayOutputStream out, int paramCount, boolean hasResult) {
+ write(out, 0x60);
+ u32(out, paramCount);
+ for (int i = 0; i < paramCount; i++) {
+ write(out, 0x7f);
+ }
+ u32(out, hasResult ? 1 : 0);
+ if (hasResult) {
+ write(out, 0x7f);
+ }
+ }
+
+ private static void export(ByteArrayOutputStream out, String name, int kind, int index) {
+ byte[] nameBytes = name.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+ u32(out, nameBytes.length);
+ out.writeBytes(nameBytes);
+ write(out, kind);
+ u32(out, index);
+ }
+
+ private static void code(ByteArrayOutputStream out, int... body) {
+ u32(out, body.length);
+ write(out, body);
+ }
+
+ private static void section(ByteArrayOutputStream wasm, int id, SectionWriter writer) {
+ var body = new ByteArrayOutputStream();
+ writer.write(body);
+ write(wasm, id);
+ u32(wasm, body.size());
+ wasm.writeBytes(body.toByteArray());
+ }
+
+ private static void u32(ByteArrayOutputStream out, int value) {
+ int remaining = value;
+ do {
+ int b = remaining & 0x7f;
+ remaining >>>= 7;
+ if (remaining != 0) {
+ b |= 0x80;
+ }
+ write(out, b);
+ } while (remaining != 0);
+ }
+
+ private static void write(ByteArrayOutputStream out, int... bytes) {
+ for (int b : bytes) {
+ out.write(b);
+ }
+ }
+
+ private static byte[] hex(String hex) {
+ byte[] out = new byte[hex.length() / 2];
+ for (int i = 0; i < out.length; i++) {
+ out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
+ }
+ return out;
+ }
+
+ private interface SectionWriter {
+ void write(ByteArrayOutputStream out);
+ }
+}
diff --git a/zeroj-bbs/README.md b/zeroj-bbs/README.md
new file mode 100644
index 0000000..733ac2c
--- /dev/null
+++ b/zeroj-bbs/README.md
@@ -0,0 +1,129 @@
+# zeroj-bbs
+
+CFRG BBS draft-10 signatures and selective disclosure for ZeroJ.
+
+This module implements the BLS12-381 ciphersuites from
+`draft-irtf-cfrg-bbs-signatures-10`:
+
+```text
+BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_
+BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_
+```
+
+Implemented operations:
+
+- `KeyGen`
+- `SkToPk`
+- `Sign`
+- `Verify`
+- `ProofGen`
+- `ProofVerify`
+
+The implementation is vector-tested against the official draft-10 SHA-256 and
+SHAKE-256 vectors for key generation, public-key derivation, message scalar
+mapping, generator derivation, signatures, proof generation, proof verification,
+hash-to-scalar, and mocked random scalars. The tests cover the draft fixture JSON
+for both ciphersuites: 10 signature cases and 15 proof cases per ciphersuite.
+
+## Basic Use
+
+```java
+var service = BbsService.pureJava();
+
+// Optional SHAKE-256 ciphersuite:
+var shakeService = BbsService.pureJava(BbsCiphersuite.BLS12381_SHAKE256);
+
+// Optional explicit BLS12-381 provider selection:
+var serviceWithBlsProvider = BbsService.withBlsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ blsProvider);
+
+var keyPair = service.keyPair(keyMaterial, keyInfo);
+var signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+
+boolean signatureValid = service.verify(keyPair.publicKey(), signature, messages, header);
+
+var presentation = service.derivePresentation(
+ keyPair.publicKey(),
+ signature,
+ messages,
+ header,
+ presentationHeader,
+ new int[]{0, 2});
+
+boolean proofValid = service.verifyPresentation(keyPair.publicKey(), presentation);
+```
+
+`messages`, `header`, and `presentationHeader` are byte arrays. Revealed indexes
+are zero-based and must be strictly ascending.
+
+## Presentation Encoding
+
+`BbsPresentationCodec` wraps draft-compatible proof bytes in a small
+deterministic CBOR envelope:
+
+```java
+byte[] cbor = BbsPresentationCodec.encode(presentation);
+BbsPresentation decoded = BbsPresentationCodec.decode(cbor);
+```
+
+Use proof format:
+
+```text
+bbs-cfrg-draft10-presentation-cbor-v1
+```
+
+## ZeroJ Verifier
+
+`BbsZkVerifier` verifies `ZkProofEnvelope` values with:
+
+- `ProofSystemId.BBS`
+- `CurveId.BLS12_381`
+- `proofFormat = bbs-cfrg-draft10-presentation-cbor-v1`
+- verification material `vkBytes = issuer BBS public key`
+
+The verifier checks cryptographic validity only. Issuer trust, schema checks,
+expiration, revocation, holder binding, and disclosure policy remain application
+policy.
+
+## WASM Provider
+
+`zeroj-bbs-wasm` provides an explicit opt-in provider:
+
+```java
+var provider = com.bloxbean.cardano.zeroj.bbs.wasm.WasmBbsProvider.createDefault();
+var service = new BbsService(provider);
+```
+
+It uses the same BBS draft implementation with BLS12-381 operations backed by
+the Rust/Chicory `zeroj-bls12381-wasm` module.
+
+## Native blst BLS Provider
+
+`zeroj-blst` exposes a native-backed BLS12-381 provider that can be selected
+without changing the BBS API:
+
+```java
+var bls = com.bloxbean.cardano.zeroj.blst.BlstBls12381Provider.createDefault();
+var service = BbsService.withBlsProvider(BbsCiphersuite.BLS12381_SHA256, bls);
+```
+
+The `zeroj-bbs` conformance tests run the same official draft-10 signature and
+proof vectors against the pure Java, WASM, and blst BLS providers.
+
+## Production Hardening
+
+- SHA-256 and SHAKE-256 ciphersuites are implemented and pass official
+ draft-10 fixture vectors.
+- BBS secret-key, signature, proof-randomness, and hidden-message scalar
+ multiplications go through the explicit `Bls12381Provider` secret-scalar
+ boundary.
+- The pure Java provider backs that boundary with fixed-schedule Jacobian scalar
+ multiplication and Montgomery-form scalar inversion. As with any JVM
+ cryptographic implementation, high-value deployments should still run an
+ environment-specific side-channel review.
+
+References:
+
+-
+-
diff --git a/zeroj-bbs/build.gradle b/zeroj-bbs/build.gradle
new file mode 100644
index 0000000..169954a
--- /dev/null
+++ b/zeroj-bbs/build.gradle
@@ -0,0 +1,27 @@
+plugins {
+ id 'java-library'
+}
+
+description = 'ZeroJ CFRG BBS signatures and selective disclosure'
+
+dependencies {
+ api project(':zeroj-api')
+ api project(':zeroj-backend-spi')
+ api project(':zeroj-bls12381')
+ implementation 'co.nstant.in:cbor:0.9'
+
+ testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
+ testImplementation project(':zeroj-bls12381-wasm')
+ testImplementation project(':zeroj-blst')
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ pom {
+ name = 'ZeroJ BBS'
+ description = 'Pure Java CFRG BBS signatures and selective disclosure'
+ }
+ }
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsCiphersuite.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsCiphersuite.java
new file mode 100644
index 0000000..7ef0e99
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsCiphersuite.java
@@ -0,0 +1,81 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * CFRG BBS ciphersuites supported by ZeroJ.
+ */
+public enum BbsCiphersuite {
+ BLS12381_SHA256(
+ "BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_",
+ "a8ce256102840821a3e94ea9025e4662b205762f9776b3a766c872b948f1fd225e7c59698588e70d11406d161b4e28c9"),
+ BLS12381_SHAKE256(
+ "BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_",
+ "8929dfbc7e6642c4ed9cba0856e493f8b9d7d5fcb0c31ef8fdcd34d50648a56c795e106e9eada6e0bda386b414150755");
+
+ public static final String DEFAULT_PROOF_FORMAT = "bbs-cfrg-draft10-presentation-cbor-v1";
+
+ private final String ciphersuiteId;
+ private final byte[] ciphersuiteIdBytes;
+ private final byte[] apiId;
+ private final G1Point p1;
+
+ BbsCiphersuite(String ciphersuiteId, String p1CompressedHex) {
+ this.ciphersuiteId = ciphersuiteId;
+ this.ciphersuiteIdBytes = ciphersuiteId.getBytes(StandardCharsets.US_ASCII);
+ this.apiId = (ciphersuiteId + "H2G_HM2S_").getBytes(StandardCharsets.US_ASCII);
+ this.p1 = Bls12381Codecs.g1FromCompressed(hexToBytes(p1CompressedHex));
+ }
+
+ public String ciphersuiteId() {
+ return ciphersuiteId;
+ }
+
+ public byte[] ciphersuiteIdBytes() {
+ return ciphersuiteIdBytes.clone();
+ }
+
+ public byte[] apiId() {
+ return apiId.clone();
+ }
+
+ public G1Point p1() {
+ return p1;
+ }
+
+ public int scalarBytes() {
+ return Bls12381Codecs.SCALAR_BYTES;
+ }
+
+ public int g1Bytes() {
+ return Bls12381Codecs.G1_COMPRESSED_BYTES;
+ }
+
+ public int g2Bytes() {
+ return Bls12381Codecs.G2_COMPRESSED_BYTES;
+ }
+
+ public int expandLen() {
+ return 48;
+ }
+
+ public static BbsCiphersuite fromCiphersuiteId(String ciphersuiteId) {
+ for (BbsCiphersuite ciphersuite : values()) {
+ if (ciphersuite.ciphersuiteId.equals(ciphersuiteId)) {
+ return ciphersuite;
+ }
+ }
+ throw new IllegalArgumentException("Unknown BBS ciphersuite: " + ciphersuiteId);
+ }
+
+ private static byte[] hexToBytes(String hex) {
+ byte[] out = new byte[hex.length() / 2];
+ for (int i = 0; i < out.length; i++) {
+ out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
+ }
+ return out;
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsException.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsException.java
new file mode 100644
index 0000000..5f64323
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsException.java
@@ -0,0 +1,14 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+/**
+ * Runtime exception raised by CFRG BBS operations.
+ */
+public class BbsException extends RuntimeException {
+ public BbsException(String message) {
+ super(message);
+ }
+
+ public BbsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsKeyPair.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsKeyPair.java
new file mode 100644
index 0000000..861af00
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsKeyPair.java
@@ -0,0 +1,16 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.util.Objects;
+
+/**
+ * CFRG BBS key pair.
+ */
+public record BbsKeyPair(BbsSecretKey secretKey, BbsPublicKey publicKey) {
+ public BbsKeyPair {
+ Objects.requireNonNull(secretKey, "secretKey required");
+ Objects.requireNonNull(publicKey, "publicKey required");
+ if (secretKey.ciphersuite() != publicKey.ciphersuite()) {
+ throw new IllegalArgumentException("secret and public key ciphersuites differ");
+ }
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentation.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentation.java
new file mode 100644
index 0000000..dbc26ba
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentation.java
@@ -0,0 +1,31 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CFRG BBS selective-disclosure presentation.
+ */
+public record BbsPresentation(
+ BbsProof proof,
+ byte[] header,
+ byte[] presentationHeader,
+ List revealedMessages
+) {
+ public BbsPresentation {
+ Objects.requireNonNull(proof, "proof required");
+ header = header != null ? header.clone() : new byte[0];
+ presentationHeader = presentationHeader != null ? presentationHeader.clone() : new byte[0];
+ revealedMessages = List.copyOf(Objects.requireNonNull(revealedMessages, "revealedMessages required"));
+ }
+
+ @Override
+ public byte[] header() {
+ return header.clone();
+ }
+
+ @Override
+ public byte[] presentationHeader() {
+ return presentationHeader.clone();
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentationCodec.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentationCodec.java
new file mode 100644
index 0000000..898d720
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentationCodec.java
@@ -0,0 +1,264 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import co.nstant.in.cbor.CborBuilder;
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.CborEncoder;
+import co.nstant.in.cbor.CborException;
+import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.ByteString;
+import co.nstant.in.cbor.model.DataItem;
+import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Deterministic CBOR wrapper for draft-compatible BBS proof presentations.
+ *
+ *
+ */
+public final class BbsPresentationCodec {
+ private static final int VERSION = 1;
+ private static final int MAX_ENVELOPE_BYTES = 1024 * 1024;
+ private static final int MAX_HEADER_BYTES = 65_535;
+ private static final int MAX_PRESENTATION_HEADER_BYTES = 65_535;
+ private static final int MAX_REVEALED_MESSAGE_BYTES = 65_535;
+ private static final int MAX_MESSAGES = 1024;
+ private static final int MAX_CIPHERSUITE_ID_CHARS = 128;
+
+ private BbsPresentationCodec() {}
+
+ public static byte[] encode(BbsPresentation presentation) {
+ Objects.requireNonNull(presentation, "presentation required");
+ try {
+ BbsProof proof = presentation.proof();
+ validateProofLength(proof.bytes(), proof.ciphersuite());
+ requireMaxLength(presentation.header(), MAX_HEADER_BYTES, "BBS header");
+ requireMaxLength(presentation.presentationHeader(), MAX_PRESENTATION_HEADER_BYTES, "BBS presentation header");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new CborEncoder(baos).encode(new CborBuilder()
+ .addMap()
+ .put(new UnsignedInteger(1), new UnsignedInteger(VERSION))
+ .put(new UnsignedInteger(2), new UnicodeString(proof.ciphersuite().ciphersuiteId()))
+ .put(new UnsignedInteger(3), new ByteString(proof.bytes()))
+ .put(new UnsignedInteger(4), new ByteString(presentation.header()))
+ .put(new UnsignedInteger(5), new ByteString(presentation.presentationHeader()))
+ .put(new UnsignedInteger(6), encodeRevealedMessages(presentation.revealedMessages()))
+ .end()
+ .build());
+ return baos.toByteArray();
+ } catch (CborException e) {
+ throw new BbsException("Failed to encode BBS presentation CBOR", e);
+ }
+ }
+
+ /**
+ * Decode a canonical deterministic CBOR BBS presentation.
+ *
+ *
The decoder rejects byte-distinct non-canonical encodings of the same
+ * logical presentation by decoding and re-encoding with the canonical encoder.
+ * Callers that hash, sign, or content-address presentation envelopes can rely
+ * on this method accepting only ZeroJ's canonical envelope form.
+ */
+ public static BbsPresentation decode(byte[] cbor) {
+ return decodeStrict(cbor);
+ }
+
+ public static BbsPresentation decodeStrict(byte[] cbor) {
+ return decodeInternal(cbor, true);
+ }
+
+ private static BbsPresentation decodeInternal(byte[] cbor, boolean strict) {
+ try {
+ validateEnvelopeBytes(cbor);
+ CborDecoder decoder = new CborDecoder(new ByteArrayInputStream(cbor));
+ decoder.setRejectDuplicateKeys(true);
+ decoder.setMaxPreallocationSize(MAX_ENVELOPE_BYTES);
+ List items = decoder.decode();
+ if (items.size() != 1 || !(items.getFirst() instanceof co.nstant.in.cbor.model.Map map)) {
+ throw new BbsException("BBS presentation CBOR must be a map");
+ }
+ if (map.getKeys().size() != 6) {
+ throw new BbsException("BBS presentation CBOR map must contain exactly 6 keys");
+ }
+ int version = intAt(map, 1);
+ if (version != VERSION) {
+ throw new BbsException("Unsupported BBS presentation CBOR version: " + version);
+ }
+ BbsCiphersuite ciphersuite = BbsCiphersuite.fromCiphersuiteId(textAt(map, 2));
+ byte[] proofBytes = bytesAt(map, 3, "BBS proof", maxProofBytes(ciphersuite));
+ BbsProof proof = new BbsProof(proofBytes, ciphersuite);
+ byte[] header = bytesAt(map, 4, "BBS header", MAX_HEADER_BYTES);
+ byte[] presentationHeader = bytesAt(map, 5, "BBS presentation header", MAX_PRESENTATION_HEADER_BYTES);
+ List revealedMessages = decodeRevealedMessages(arrayAt(map, 6));
+ validateProofAndRevealedMessageCount(proof.bytes(), ciphersuite, revealedMessages.size());
+ BbsPresentation presentation = new BbsPresentation(proof, header, presentationHeader, revealedMessages);
+ if (strict && !Arrays.equals(cbor, encode(presentation))) {
+ throw new BbsException("BBS presentation CBOR must be canonical");
+ }
+ return presentation;
+ } catch (BbsException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new BbsException("Failed to decode BBS presentation CBOR", e);
+ }
+ }
+
+ private static Array encodeRevealedMessages(List messages) {
+ Objects.requireNonNull(messages, "revealed messages required");
+ if (messages.size() > MAX_MESSAGES) {
+ throw new BbsException("BBS revealed message count exceeds " + MAX_MESSAGES);
+ }
+ Array outer = new Array();
+ int previous = -1;
+ for (BbsRevealedMessage message : messages) {
+ if (message.index() <= previous) {
+ throw new BbsException("BBS revealed message indexes must be strictly ascending");
+ }
+ previous = message.index();
+ requireMaxLength(message.message(), MAX_REVEALED_MESSAGE_BYTES, "BBS revealed message");
+ Array item = new Array();
+ item.add(new UnsignedInteger(message.index()));
+ item.add(new ByteString(message.message()));
+ outer.add(item);
+ }
+ return outer;
+ }
+
+ private static List decodeRevealedMessages(Array array) {
+ if (array.getDataItems().size() > MAX_MESSAGES) {
+ throw new BbsException("BBS revealed message count exceeds " + MAX_MESSAGES);
+ }
+ List out = new ArrayList<>();
+ int previous = -1;
+ for (DataItem item : array.getDataItems()) {
+ if (!(item instanceof Array pair) || pair.getDataItems().size() != 2) {
+ throw new BbsException("BBS revealed message must be [index, message]");
+ }
+ int index = intFromItem(pair.getDataItems().get(0), "BBS revealed message index");
+ if (index <= previous) {
+ throw new BbsException("BBS revealed message indexes must be strictly ascending");
+ }
+ previous = index;
+ byte[] message = bytesFromItem(
+ pair.getDataItems().get(1),
+ "BBS revealed message",
+ MAX_REVEALED_MESSAGE_BYTES);
+ out.add(new BbsRevealedMessage(index, message));
+ }
+ return List.copyOf(out);
+ }
+
+ private static int intAt(co.nstant.in.cbor.model.Map map, int key) {
+ return intFromItem(itemAt(map, key), "BBS presentation CBOR map key " + key);
+ }
+
+ private static String textAt(co.nstant.in.cbor.model.Map map, int key) {
+ DataItem item = itemAt(map, key);
+ if (!(item instanceof UnicodeString text)) {
+ throw new BbsException("BBS presentation CBOR map key " + key + " must be text");
+ }
+ String value = text.getString();
+ if (value.length() > MAX_CIPHERSUITE_ID_CHARS) {
+ throw new BbsException("BBS ciphersuite id is too long");
+ }
+ return value;
+ }
+
+ private static byte[] bytesAt(co.nstant.in.cbor.model.Map map, int key, String label, int maxLength) {
+ return bytesFromItem(itemAt(map, key), label, maxLength);
+ }
+
+ private static Array arrayAt(co.nstant.in.cbor.model.Map map, int key) {
+ DataItem item = itemAt(map, key);
+ if (!(item instanceof Array array)) {
+ throw new BbsException("BBS presentation CBOR map key " + key + " must be an array");
+ }
+ return array;
+ }
+
+ private static DataItem itemAt(co.nstant.in.cbor.model.Map map, int key) {
+ DataItem item = map.get(new UnsignedInteger(key));
+ if (item == null) {
+ throw new BbsException("Missing BBS presentation CBOR map key: " + key);
+ }
+ return item;
+ }
+
+ private static int intFromItem(DataItem item, String label) {
+ if (!(item instanceof UnsignedInteger number)) {
+ throw new BbsException(label + " must be an unsigned integer");
+ }
+ BigInteger value = number.getValue();
+ if (value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
+ throw new BbsException(label + " is too large");
+ }
+ return value.intValueExact();
+ }
+
+ private static byte[] bytesFromItem(DataItem item, String label, int maxLength) {
+ if (!(item instanceof ByteString bytes)) {
+ throw new BbsException(label + " must be a byte string");
+ }
+ byte[] value = bytes.getBytes();
+ requireMaxLength(value, maxLength, label);
+ return value.clone();
+ }
+
+ private static void validateEnvelopeBytes(byte[] cbor) {
+ Objects.requireNonNull(cbor, "CBOR bytes required");
+ requireMaxLength(cbor, MAX_ENVELOPE_BYTES, "BBS presentation CBOR");
+ }
+
+ private static void requireMaxLength(byte[] bytes, int maxLength, String label) {
+ Objects.requireNonNull(bytes, label + " required");
+ if (bytes.length > maxLength) {
+ throw new BbsException(label + " exceeds " + maxLength + " bytes");
+ }
+ }
+
+ private static void validateProofLength(byte[] proofBytes, BbsCiphersuite ciphersuite) {
+ requireMaxLength(proofBytes, maxProofBytes(ciphersuite), "BBS proof");
+ hiddenMessageCount(proofBytes, ciphersuite);
+ }
+
+ private static void validateProofAndRevealedMessageCount(
+ byte[] proofBytes,
+ BbsCiphersuite ciphersuite,
+ int revealedCount) {
+ int hiddenCount = hiddenMessageCount(proofBytes, ciphersuite);
+ if (hiddenCount + revealedCount > MAX_MESSAGES) {
+ throw new BbsException("BBS presentation message count exceeds " + MAX_MESSAGES);
+ }
+ }
+
+ private static int hiddenMessageCount(byte[] proofBytes, BbsCiphersuite ciphersuite) {
+ int floor = 3 * ciphersuite.g1Bytes() + 4 * ciphersuite.scalarBytes();
+ if (proofBytes.length < floor) {
+ throw new BbsException("BBS proof is too short: " + proofBytes.length);
+ }
+ int scalarBytes = proofBytes.length - 3 * ciphersuite.g1Bytes();
+ if (scalarBytes % ciphersuite.scalarBytes() != 0) {
+ throw new BbsException("BBS proof scalar section is not aligned to 32-byte scalars");
+ }
+ return scalarBytes / ciphersuite.scalarBytes() - 4;
+ }
+
+ private static int maxProofBytes(BbsCiphersuite ciphersuite) {
+ return 3 * ciphersuite.g1Bytes() + (4 + MAX_MESSAGES) * ciphersuite.scalarBytes();
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsProof.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsProof.java
new file mode 100644
index 0000000..37cb470
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsProof.java
@@ -0,0 +1,46 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * CFRG BBS proof octets.
+ */
+public final class BbsProof {
+ private final byte[] bytes;
+ private final BbsCiphersuite ciphersuite;
+
+ public BbsProof(byte[] bytes, BbsCiphersuite ciphersuite) {
+ Objects.requireNonNull(bytes, "proof bytes required");
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ int floor = 3 * ciphersuite.g1Bytes() + 4 * ciphersuite.scalarBytes();
+ if (bytes.length < floor) {
+ throw new IllegalArgumentException("BBS proof is too short: " + bytes.length);
+ }
+ if ((bytes.length - floor) % ciphersuite.scalarBytes() != 0) {
+ throw new IllegalArgumentException("BBS proof scalar section is not aligned to 32-byte scalars");
+ }
+ this.bytes = bytes.clone();
+ this.ciphersuite = ciphersuite;
+ }
+
+ public byte[] bytes() {
+ return bytes.clone();
+ }
+
+ public BbsCiphersuite ciphersuite() {
+ return ciphersuite;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof BbsProof p
+ && ciphersuite == p.ciphersuite
+ && Arrays.equals(bytes, p.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Arrays.hashCode(bytes) + ciphersuite.hashCode();
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPublicKey.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPublicKey.java
new file mode 100644
index 0000000..42d4335
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPublicKey.java
@@ -0,0 +1,49 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * CFRG BBS public key octets.
+ */
+public final class BbsPublicKey {
+ private final byte[] bytes;
+ private final BbsCiphersuite ciphersuite;
+
+ public BbsPublicKey(byte[] bytes, BbsCiphersuite ciphersuite) {
+ this.bytes = requireNonEmpty(bytes, "public key").clone();
+ this.ciphersuite = Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ if (this.bytes.length != ciphersuite.g2Bytes()) {
+ throw new IllegalArgumentException("BBS public key must be " + ciphersuite.g2Bytes()
+ + " bytes, got " + this.bytes.length);
+ }
+ }
+
+ public byte[] bytes() {
+ return bytes.clone();
+ }
+
+ public BbsCiphersuite ciphersuite() {
+ return ciphersuite;
+ }
+
+ private static byte[] requireNonEmpty(byte[] bytes, String label) {
+ Objects.requireNonNull(bytes, label + " required");
+ if (bytes.length == 0) {
+ throw new IllegalArgumentException(label + " must not be empty");
+ }
+ return bytes;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof BbsPublicKey k
+ && ciphersuite == k.ciphersuite
+ && Arrays.equals(bytes, k.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Arrays.hashCode(bytes) + ciphersuite.hashCode();
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsRevealedMessage.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsRevealedMessage.java
new file mode 100644
index 0000000..2f5b02c
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsRevealedMessage.java
@@ -0,0 +1,41 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Revealed message and original message index for a BBS presentation.
+ */
+public final class BbsRevealedMessage {
+ private final int index;
+ private final byte[] message;
+
+ public BbsRevealedMessage(int index, byte[] message) {
+ if (index < 0) {
+ throw new IllegalArgumentException("index must be non-negative");
+ }
+ Objects.requireNonNull(message, "message required");
+ this.index = index;
+ this.message = message.clone();
+ }
+
+ public int index() {
+ return index;
+ }
+
+ public byte[] message() {
+ return message.clone();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof BbsRevealedMessage m
+ && index == m.index
+ && Arrays.equals(message, m.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * index + Arrays.hashCode(message);
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSecretKey.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSecretKey.java
new file mode 100644
index 0000000..3324bbd
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSecretKey.java
@@ -0,0 +1,19 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * CFRG BBS secret key scalar.
+ */
+public record BbsSecretKey(BigInteger value, BbsCiphersuite ciphersuite) {
+ public BbsSecretKey {
+ Objects.requireNonNull(value, "value required");
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ com.bloxbean.cardano.zeroj.bbs.internal.BbsCodec.scalarToBytes(value);
+ }
+
+ public byte[] toBytes() {
+ return com.bloxbean.cardano.zeroj.bbs.internal.BbsCodec.scalarToBytes(value);
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsService.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsService.java
new file mode 100644
index 0000000..714cf2c
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsService.java
@@ -0,0 +1,134 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import com.bloxbean.cardano.zeroj.bbs.internal.CfrgBbsCore;
+import com.bloxbean.cardano.zeroj.bbs.spi.BbsProvider;
+import com.bloxbean.cardano.zeroj.bbs.spi.BbsProviders;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+
+import java.security.SecureRandom;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * High-level CFRG BBS draft-10 service.
+ */
+public final class BbsService {
+ private final BbsProvider provider;
+ private final SecureRandom random;
+
+ public BbsService(BbsProvider provider) {
+ this(provider, new SecureRandom());
+ }
+
+ public BbsService(BbsProvider provider, SecureRandom random) {
+ this.provider = Objects.requireNonNull(provider, "provider required");
+ this.random = Objects.requireNonNull(random, "secure random required");
+ }
+
+ public static BbsService pureJava() {
+ return new BbsService(BbsProviders.pureJava());
+ }
+
+ public static BbsService pureJava(BbsCiphersuite ciphersuite) {
+ return new BbsService(BbsProviders.pureJava(ciphersuite));
+ }
+
+ public static BbsService withBlsProvider(BbsCiphersuite ciphersuite, Bls12381Provider bls) {
+ return new BbsService(BbsProviders.withBlsProvider(ciphersuite, bls));
+ }
+
+ public BbsProvider provider() {
+ return provider;
+ }
+
+ public BbsKeyPair keyPair(byte[] keyMaterial, byte[] keyInfo) {
+ return provider.keyPair(keyMaterial, keyInfo);
+ }
+
+ /**
+ * Sign messages with the ZeroJ argument order: messages before header.
+ *
+ *
For the draft-10 order, use {@link #sign(BbsSecretKey, BbsPublicKey, byte[], List)}.
+ */
+ public BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, List messages, byte[] header) {
+ return provider.sign(secretKey, publicKey, messages, header);
+ }
+
+ /**
+ * Sign messages with the draft-10 argument order: header before messages.
+ */
+ public BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, byte[] header, List messages) {
+ return sign(secretKey, publicKey, messages, header);
+ }
+
+ /**
+ * Verify a signature with the ZeroJ argument order: messages before header.
+ *
+ *
For the draft-10 order, use {@link #verify(BbsPublicKey, BbsSignature, byte[], List)}.
+ */
+ public boolean verify(BbsPublicKey publicKey, BbsSignature signature, List messages, byte[] header) {
+ return provider.verify(publicKey, signature, messages, header);
+ }
+
+ /**
+ * Verify a signature with the draft-10 argument order: header before messages.
+ */
+ public boolean verify(BbsPublicKey publicKey, BbsSignature signature, byte[] header, List messages) {
+ return verify(publicKey, signature, messages, header);
+ }
+
+ public BbsPresentation derivePresentation(
+ BbsPublicKey publicKey,
+ BbsSignature signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes) {
+ Objects.requireNonNull(messages, "messages required");
+ int[] indexes = validateDisclosedIndexes(disclosedIndexes, messages.size());
+ BbsProof proof = provider.proofGen(
+ publicKey, signature, messages, header, presentationHeader, indexes, random);
+ List revealedMessages = revealedMessages(messages, indexes);
+ return new BbsPresentation(proof, header, presentationHeader, revealedMessages);
+ }
+
+ public boolean verifyPresentation(BbsPublicKey publicKey, BbsPresentation presentation) {
+ Objects.requireNonNull(presentation, "presentation required");
+ List revealed = presentation.revealedMessages().stream()
+ .sorted(Comparator.comparingInt(BbsRevealedMessage::index))
+ .toList();
+ int[] indexes = revealed.stream().mapToInt(BbsRevealedMessage::index).toArray();
+ validateDisclosedIndexes(indexes, hiddenMessageCountFromProof(presentation.proof()) + revealed.size());
+ List messages = revealed.stream().map(BbsRevealedMessage::message).toList();
+ return provider.proofVerify(
+ publicKey,
+ presentation.proof(),
+ presentation.header(),
+ presentation.presentationHeader(),
+ messages,
+ indexes);
+ }
+
+ private static List revealedMessages(List messages, int[] indexes) {
+ Objects.requireNonNull(messages, "messages required");
+ return java.util.Arrays.stream(indexes)
+ .mapToObj(index -> new BbsRevealedMessage(index, messages.get(index)))
+ .toList();
+ }
+
+ private static int[] validateDisclosedIndexes(int[] indexes, int messageCount) {
+ try {
+ return CfrgBbsCore.validateDisclosedIndexes(indexes, messageCount);
+ } catch (RuntimeException e) {
+ throw new BbsException("Invalid BBS disclosed indexes", e);
+ }
+ }
+
+ private static int hiddenMessageCountFromProof(BbsProof proof) {
+ BbsCiphersuite ciphersuite = proof.ciphersuite();
+ int scalarBytes = proof.bytes().length - 3 * ciphersuite.g1Bytes();
+ int scalarCount = scalarBytes / ciphersuite.scalarBytes();
+ return scalarCount - 4;
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSignature.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSignature.java
new file mode 100644
index 0000000..8e22d48
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSignature.java
@@ -0,0 +1,43 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * CFRG BBS signature octets.
+ */
+public final class BbsSignature {
+ private final byte[] bytes;
+ private final BbsCiphersuite ciphersuite;
+
+ public BbsSignature(byte[] bytes, BbsCiphersuite ciphersuite) {
+ Objects.requireNonNull(bytes, "signature bytes required");
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ if (bytes.length != ciphersuite.g1Bytes() + ciphersuite.scalarBytes()) {
+ throw new IllegalArgumentException("BBS signature must be "
+ + (ciphersuite.g1Bytes() + ciphersuite.scalarBytes()) + " bytes, got " + bytes.length);
+ }
+ this.bytes = bytes.clone();
+ this.ciphersuite = ciphersuite;
+ }
+
+ public byte[] bytes() {
+ return bytes.clone();
+ }
+
+ public BbsCiphersuite ciphersuite() {
+ return ciphersuite;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof BbsSignature s
+ && ciphersuite == s.ciphersuite
+ && Arrays.equals(bytes, s.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Arrays.hashCode(bytes) + ciphersuite.hashCode();
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/BbsCodec.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/BbsCodec.java
new file mode 100644
index 0000000..8c597ac
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/BbsCodec.java
@@ -0,0 +1,298 @@
+package com.bloxbean.cardano.zeroj.bbs.internal;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Draft-10 BBS octet serialization helpers.
+ */
+public final class BbsCodec {
+ public static final BigInteger R = Bls12381Generators.SCALAR_FIELD_ORDER;
+
+ private BbsCodec() {}
+
+ public record SignatureParts(G1Point a, BigInteger e) {
+ public SignatureParts {
+ requireNonIdentity(a, "signature A");
+ e = requireNonZeroScalar(e, "signature e");
+ }
+ }
+
+ public record ProofParts(
+ G1Point aBar,
+ G1Point bBar,
+ G1Point d,
+ BigInteger eHat,
+ BigInteger r1Hat,
+ BigInteger r3Hat,
+ List mHats,
+ BigInteger challenge
+ ) {
+ public ProofParts {
+ requireNonIdentity(aBar, "proof Abar");
+ requireNonIdentity(bBar, "proof Bbar");
+ requireNonIdentity(d, "proof D");
+ eHat = requireScalar(eHat, "proof eHat");
+ r1Hat = requireScalar(r1Hat, "proof r1Hat");
+ r3Hat = requireScalar(r3Hat, "proof r3Hat");
+ mHats = List.copyOf(Objects.requireNonNull(mHats, "proof commitments required"));
+ for (BigInteger mHat : mHats) {
+ requireScalar(mHat, "proof hidden message commitment");
+ }
+ challenge = requireScalar(challenge, "proof challenge");
+ }
+ }
+
+ public static byte[] scalarToBytes(BigInteger scalar) {
+ return fixedBigEndian(requireNonZeroScalar(scalar, "scalar"), Bls12381Codecs.SCALAR_BYTES);
+ }
+
+ public static byte[] scalarToBytesAllowZero(BigInteger scalar) {
+ return fixedBigEndian(requireScalar(scalar, "scalar"), Bls12381Codecs.SCALAR_BYTES);
+ }
+
+ public static BigInteger scalarFromBytes(byte[] bytes, String label) {
+ requireLength(bytes, Bls12381Codecs.SCALAR_BYTES, label);
+ BigInteger scalar = new BigInteger(1, bytes);
+ if (scalar.compareTo(R) >= 0) {
+ throw new IllegalArgumentException(label + " is outside BLS12-381 Fr");
+ }
+ return scalar;
+ }
+
+ public static BigInteger nonZeroScalarFromBytes(byte[] bytes, String label) {
+ return requireNonZeroScalar(scalarFromBytes(bytes, label), label);
+ }
+
+ public static byte[] publicKeyToOctets(G2Point point) {
+ requireNonIdentity(point, "public key");
+ return Bls12381Codecs.g2ToCompressed(Bls12381Codecs.requireValid(point));
+ }
+
+ public static G2Point octetsToPublicKey(byte[] bytes) {
+ requireLength(bytes, Bls12381Codecs.G2_COMPRESSED_BYTES, "public key");
+ G2Point point = Bls12381Codecs.g2FromCompressed(bytes);
+ if (point.isInfinity()) {
+ throw new IllegalArgumentException("BBS public key must not be identity");
+ }
+ return point;
+ }
+
+ public static byte[] signatureToOctets(SignatureParts signature) {
+ Objects.requireNonNull(signature, "signature required");
+ return concat(
+ Bls12381Codecs.g1ToCompressed(signature.a()),
+ scalarToBytes(signature.e()));
+ }
+
+ public static SignatureParts octetsToSignature(byte[] bytes, BbsCiphersuite ciphersuite) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ requireLength(bytes, ciphersuite.g1Bytes() + ciphersuite.scalarBytes(), "signature");
+ G1Point a = Bls12381Codecs.g1FromCompressed(Arrays.copyOfRange(bytes, 0, ciphersuite.g1Bytes()));
+ if (a.isInfinity()) {
+ throw new IllegalArgumentException("BBS signature A must not be identity");
+ }
+ BigInteger e = nonZeroScalarFromBytes(
+ Arrays.copyOfRange(bytes, ciphersuite.g1Bytes(), bytes.length), "signature e");
+ return new SignatureParts(a, e);
+ }
+
+ public static byte[] proofToOctets(ProofParts proof) {
+ Objects.requireNonNull(proof, "proof required");
+ List parts = new ArrayList<>();
+ parts.add(Bls12381Codecs.g1ToCompressed(proof.aBar()));
+ parts.add(Bls12381Codecs.g1ToCompressed(proof.bBar()));
+ parts.add(Bls12381Codecs.g1ToCompressed(proof.d()));
+ parts.add(scalarToBytesAllowZero(proof.eHat()));
+ parts.add(scalarToBytesAllowZero(proof.r1Hat()));
+ parts.add(scalarToBytesAllowZero(proof.r3Hat()));
+ for (BigInteger mHat : proof.mHats()) {
+ parts.add(scalarToBytesAllowZero(mHat));
+ }
+ parts.add(scalarToBytesAllowZero(proof.challenge()));
+ return concat(parts.toArray(byte[][]::new));
+ }
+
+ public static ProofParts octetsToProof(byte[] bytes, BbsCiphersuite ciphersuite) {
+ Objects.requireNonNull(bytes, "proof bytes required");
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ int floor = 3 * ciphersuite.g1Bytes() + 4 * ciphersuite.scalarBytes();
+ if (bytes.length < floor) {
+ throw new IllegalArgumentException("BBS proof must be at least " + floor + " bytes, got " + bytes.length);
+ }
+ int scalarBytesLen = bytes.length - 3 * ciphersuite.g1Bytes();
+ if (scalarBytesLen % ciphersuite.scalarBytes() != 0) {
+ throw new IllegalArgumentException("BBS proof scalar section is not aligned to 32-byte scalars");
+ }
+ int scalarCount = scalarBytesLen / ciphersuite.scalarBytes();
+ if (scalarCount < 4) {
+ throw new IllegalArgumentException("BBS proof must contain at least 4 scalars");
+ }
+
+ int offset = 0;
+ G1Point aBar = readNonIdentityG1(bytes, offset, "proof Abar");
+ offset += ciphersuite.g1Bytes();
+ G1Point bBar = readNonIdentityG1(bytes, offset, "proof Bbar");
+ offset += ciphersuite.g1Bytes();
+ G1Point d = readNonIdentityG1(bytes, offset, "proof D");
+ offset += ciphersuite.g1Bytes();
+
+ List scalars = new ArrayList<>(scalarCount);
+ while (offset < bytes.length) {
+ scalars.add(scalarFromBytes(Arrays.copyOfRange(bytes, offset, offset + ciphersuite.scalarBytes()),
+ "proof scalar"));
+ offset += ciphersuite.scalarBytes();
+ }
+
+ List commitments = scalarCount > 4
+ ? scalars.subList(3, scalarCount - 1)
+ : List.of();
+ return new ProofParts(
+ aBar,
+ bBar,
+ d,
+ scalars.get(0),
+ scalars.get(1),
+ scalars.get(2),
+ commitments,
+ scalars.get(scalarCount - 1));
+ }
+
+ public static byte[] serialize(Object... values) {
+ List parts = new ArrayList<>(values.length);
+ for (Object value : values) {
+ if (value instanceof G1Point point) {
+ parts.add(Bls12381Codecs.g1ToCompressed(Bls12381Codecs.requireValid(point)));
+ } else if (value instanceof G2Point point) {
+ parts.add(Bls12381Codecs.g2ToCompressed(Bls12381Codecs.requireValid(point)));
+ } else if (value instanceof BigInteger scalar) {
+ parts.add(scalarToBytesAllowZero(scalar));
+ } else if (value instanceof Integer integer) {
+ parts.add(i2osp(integer.longValue(), 8));
+ } else if (value instanceof Long longValue) {
+ parts.add(i2osp(longValue, 8));
+ } else if (value instanceof String string) {
+ parts.add(string.getBytes(StandardCharsets.US_ASCII));
+ } else {
+ throw new IllegalArgumentException("Unsupported BBS serialization type: " + value);
+ }
+ }
+ return concat(parts.toArray(byte[][]::new));
+ }
+
+ public static byte[] i2osp(long value, int length) {
+ if (value < 0) {
+ throw new IllegalArgumentException("I2OSP value must be non-negative");
+ }
+ if (length <= 0 || length > 8) {
+ throw new IllegalArgumentException("I2OSP length must be between 1 and 8");
+ }
+ byte[] out = new byte[length];
+ long v = value;
+ for (int i = length - 1; i >= 0; i--) {
+ out[i] = (byte) v;
+ v >>>= 8;
+ }
+ if (v != 0) {
+ throw new IllegalArgumentException("I2OSP value does not fit in " + length + " bytes");
+ }
+ return out;
+ }
+
+ public static byte[] concat(byte[]... chunks) {
+ int len = 0;
+ for (byte[] chunk : chunks) {
+ len += chunk.length;
+ }
+ byte[] out = new byte[len];
+ int offset = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, out, offset, chunk.length);
+ offset += chunk.length;
+ }
+ return out;
+ }
+
+ public static byte[] copy(byte[] bytes) {
+ return bytes != null ? bytes.clone() : new byte[0];
+ }
+
+ public static List copyMessages(List messages) {
+ Objects.requireNonNull(messages, "messages required");
+ List copy = new ArrayList<>(messages.size());
+ for (byte[] message : messages) {
+ copy.add(copy(message));
+ }
+ return List.copyOf(copy);
+ }
+
+ static BigInteger requireScalar(BigInteger scalar, String label) {
+ Objects.requireNonNull(scalar, label + " required");
+ if (scalar.signum() < 0 || scalar.compareTo(R) >= 0) {
+ throw new IllegalArgumentException(label + " must be in [0, r)");
+ }
+ return scalar;
+ }
+
+ static BigInteger requireNonZeroScalar(BigInteger scalar, String label) {
+ requireScalar(scalar, label);
+ if (scalar.signum() == 0) {
+ throw new IllegalArgumentException(label + " must be non-zero");
+ }
+ return scalar;
+ }
+
+ static G1Point requireNonIdentity(G1Point point, String label) {
+ Bls12381Codecs.requireValid(point);
+ if (point.isInfinity()) {
+ throw new IllegalArgumentException(label + " must not be identity");
+ }
+ return point;
+ }
+
+ static G2Point requireNonIdentity(G2Point point, String label) {
+ Bls12381Codecs.requireValid(point);
+ if (point.isInfinity()) {
+ throw new IllegalArgumentException(label + " must not be identity");
+ }
+ return point;
+ }
+
+ private static G1Point readNonIdentityG1(byte[] bytes, int offset, String label) {
+ G1Point point = Bls12381Codecs.g1FromCompressed(
+ Arrays.copyOfRange(bytes, offset, offset + Bls12381Codecs.G1_COMPRESSED_BYTES));
+ if (point.isInfinity()) {
+ throw new IllegalArgumentException(label + " must not be identity");
+ }
+ return point;
+ }
+
+ private static void requireLength(byte[] bytes, int expected, String label) {
+ Objects.requireNonNull(bytes, label + " bytes required");
+ if (bytes.length != expected) {
+ throw new IllegalArgumentException(label + " must be " + expected + " bytes, got " + bytes.length);
+ }
+ }
+
+ private static byte[] fixedBigEndian(BigInteger value, int length) {
+ byte[] raw = value.toByteArray();
+ int rawStart = raw.length > 1 && raw[0] == 0 ? 1 : 0;
+ int rawLen = raw.length - rawStart;
+ if (rawLen > length) {
+ throw new IllegalArgumentException("Value does not fit in " + length + " bytes");
+ }
+ byte[] out = new byte[length];
+ System.arraycopy(raw, rawStart, out, length - rawLen, rawLen);
+ return out;
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/CfrgBbsCore.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/CfrgBbsCore.java
new file mode 100644
index 0000000..7c85951
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/CfrgBbsCore.java
@@ -0,0 +1,679 @@
+package com.bloxbean.cardano.zeroj.bbs.internal;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Hash;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * CFRG BBS draft-10 algorithms for supported BLS12-381 ciphersuites.
+ */
+public final class CfrgBbsCore {
+ private static final BigInteger R = BbsCodec.R;
+
+ private CfrgBbsCore() {}
+
+ public record Generators(G1Point q1, List h) {
+ public Generators {
+ BbsCodec.requireNonIdentity(q1, "Q_1");
+ h = List.copyOf(Objects.requireNonNull(h, "H generators required"));
+ for (G1Point point : h) {
+ BbsCodec.requireNonIdentity(point, "H generator");
+ }
+ }
+
+ public int count() {
+ return h.size() + 1;
+ }
+ }
+
+ record ProofInitResult(
+ G1Point aBar,
+ G1Point bBar,
+ G1Point d,
+ G1Point t1,
+ G1Point t2,
+ BigInteger domain
+ ) {}
+
+ public static BigInteger keyGen(BbsCiphersuite ciphersuite, byte[] keyMaterial, byte[] keyInfo) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(keyMaterial, "key material required");
+ keyInfo = BbsCodec.copy(keyInfo);
+ if (keyMaterial.length < 32) {
+ throw new IllegalArgumentException("BBS key material must be at least 32 bytes");
+ }
+ if (keyInfo.length > 65535) {
+ throw new IllegalArgumentException("BBS key info must be at most 65535 bytes");
+ }
+
+ byte[] deriveInput = BbsCodec.concat(keyMaterial, BbsCodec.i2osp(keyInfo.length, 2), keyInfo);
+ // Draft-10 interface vectors bind KeyGen to the BBS API identifier, not only the ciphersuite id.
+ BigInteger sk = hashToScalar(deriveInput, dst(ciphersuite.apiId(), "KEYGEN_DST_"), ciphersuite);
+ return BbsCodec.requireNonZeroScalar(sk, "BBS secret key");
+ }
+
+ public static byte[] skToPk(BigInteger secretKey, Bls12381Provider bls) {
+ BbsCodec.requireNonZeroScalar(secretKey, "BBS secret key");
+ G2Point publicKey = Objects.requireNonNull(bls, "BLS provider required")
+ .g2SecretScalarMul(bls.g2Generator(), secretKey);
+ return BbsCodec.publicKeyToOctets(publicKey);
+ }
+
+ public static List messagesToScalars(List messages, BbsCiphersuite ciphersuite) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ List copiedMessages = BbsCodec.copyMessages(messages);
+ byte[] mapDst = dst(ciphersuite.apiId(), "MAP_MSG_TO_SCALAR_AS_HASH_");
+ List scalars = new ArrayList<>(copiedMessages.size());
+ for (byte[] message : copiedMessages) {
+ scalars.add(hashToScalar(message, mapDst, ciphersuite));
+ }
+ return List.copyOf(scalars);
+ }
+
+ public static Generators createGenerators(int count, BbsCiphersuite ciphersuite, Bls12381Provider bls) {
+ if (count < 1) {
+ throw new IllegalArgumentException("BBS generator count must be at least 1");
+ }
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(bls, "BLS provider required");
+
+ byte[] apiId = ciphersuite.apiId();
+ byte[] seedDst = dst(apiId, "SIG_GENERATOR_SEED_");
+ byte[] generatorDst = dst(apiId, "SIG_GENERATOR_DST_");
+ byte[] generatorSeed = dst(apiId, "MESSAGE_GENERATOR_SEED");
+
+ byte[] v = expandMessage(generatorSeed, seedDst, ciphersuite.expandLen(), ciphersuite);
+ List points = new ArrayList<>(count);
+ for (int i = 1; i <= count; i++) {
+ v = expandMessage(BbsCodec.concat(v, BbsCodec.i2osp(i, 8)), seedDst, ciphersuite.expandLen(), ciphersuite);
+ points.add(BbsCodec.requireNonIdentity(hashToG1(v, generatorDst, ciphersuite, bls), "BBS generator"));
+ }
+ return new Generators(points.getFirst(), points.subList(1, points.size()));
+ }
+
+ public static BigInteger calculateDomain(
+ byte[] publicKey,
+ Generators generators,
+ byte[] header,
+ BbsCiphersuite ciphersuite
+ ) {
+ Objects.requireNonNull(publicKey, "public key required");
+ Objects.requireNonNull(generators, "generators required");
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ header = BbsCodec.copy(header);
+
+ List