Skip to content

feat: extract shared BLS12-381 primitives and add CFRG BBS draft-10#3

Merged
satran004 merged 6 commits into
mainfrom
feat/bbs_wasm_impl
May 16, 2026
Merged

feat: extract shared BLS12-381 primitives and add CFRG BBS draft-10#3
satran004 merged 6 commits into
mainfrom
feat/bbs_wasm_impl

Conversation

@satran004
Copy link
Copy Markdown
Member

Per ADR-0018 and ADR-0019, restructure BLS12-381 cryptography around a neutral provider boundary and implement CFRG draft-irtf-cfrg-bbs-signatures-10 on top of it.

BLS12-381 (ADR-0018):

  • New zeroj-bls12381 module: pure-Java Fp/Fp2/Fp6/Fp12/Fr, Jacobian G1/G2, pairing, compressed/uncompressed codecs with on-curve + r-subgroup validation, RFC9380 hash-to-curve (G1 RO/NU + G2 RO/NU), Bls12381Provider SPI with explicit factory (no ServiceLoader auto-discovery).
  • Reuses existing implementations moved verbatim from zeroj-crypto and zeroj-verifier-groth16 (renames preserve git history).
  • New zeroj-bls12381-wasm: optional zkcrypto/bls12_381 Rust crate compiled to wasm32-unknown-unknown, executed via Chicory; coarse ABI (generators, scalar-mul, pairing-product); pinned rustc 1.94 + Cargo.lock checked in.
  • New BlstBls12381Provider in zeroj-blst implements the same SPI.
  • Migrate zeroj-crypto, zeroj-verifier-groth16, zeroj-verifier-plonk, zeroj-onchain-julc, and zeroj-examples to import from zeroj-bls12381.

CFRG BBS (ADR-0019):

  • New zeroj-bbs module: KeyGen, Sign, Verify, ProofGen, ProofVerify for BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_ and BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_ ciphersuites; pluggable BLS provider (pure-Java, WASM, blst).
  • Selective disclosure with Schnorr-style hidden-message responses; full subgroup validation on every untrusted point input.
  • Deterministic CBOR presentation envelope with strict-by-default decode (round-trip canonical equality), duplicate-key rejection, per-field length caps; both ZeroJ and draft-10 sign/verify argument orders.
  • BbsZkVerifier integrates with ProofSystemId.BBS for ZkProofEnvelope.
  • New zeroj-bbs-wasm scaffold and BbsSelectiveDisclosureExample.

Tests:

  • 1,765 zeroj-bls12381 tests including RFC9380 J.9/J.10 vectors, ZCash/blst compressed-generator byte vectors, off-curve and torsion rejection, oversize-DST handling, Fp2.sqrt(-1) edge case.
  • zeroj-bls12381-wasm includes a synthetic-WASM regression test pinning the response-buffer dealloc path against malformed-response leaks.
  • 100+ byte-level CFRG draft-10 fixture assertions across SHA-256 and SHAKE-256: keypair, h2s, MapMessageToScalar, generators, mockedRng, 20 signature cases, 30 proof cases, run against all three BLS providers (pure-Java, wasm-zkcrypto, blst).
  • All pre-existing Groth16/PlonK/KZG/MSM/FFT tests pass unchanged after the import migration.

satran004 and others added 6 commits May 9, 2026 21:35
Per ADR-0018 and ADR-0019, restructure BLS12-381 cryptography around a
neutral provider boundary and implement CFRG draft-irtf-cfrg-bbs-signatures-10
on top of it.

BLS12-381 (ADR-0018):
- New zeroj-bls12381 module: pure-Java Fp/Fp2/Fp6/Fp12/Fr, Jacobian G1/G2,
  pairing, compressed/uncompressed codecs with on-curve + r-subgroup
  validation, RFC9380 hash-to-curve (G1 RO/NU + G2 RO/NU), Bls12381Provider
  SPI with explicit factory (no ServiceLoader auto-discovery).
- Reuses existing implementations moved verbatim from zeroj-crypto and
  zeroj-verifier-groth16 (renames preserve git history).
- New zeroj-bls12381-wasm: optional zkcrypto/bls12_381 Rust crate compiled
  to wasm32-unknown-unknown, executed via Chicory; coarse ABI (generators,
  scalar-mul, pairing-product); pinned rustc 1.94 + Cargo.lock checked in.
- New BlstBls12381Provider in zeroj-blst implements the same SPI.
- Migrate zeroj-crypto, zeroj-verifier-groth16, zeroj-verifier-plonk,
  zeroj-onchain-julc, and zeroj-examples to import from zeroj-bls12381.

CFRG BBS (ADR-0019):
- New zeroj-bbs module: KeyGen, Sign, Verify, ProofGen, ProofVerify for
  BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_ and BBS_BLS12381G1_XOF:SHAKE-256_SSWU_RO_
  ciphersuites; pluggable BLS provider (pure-Java, WASM, blst).
- Selective disclosure with Schnorr-style hidden-message responses;
  full subgroup validation on every untrusted point input.
- Deterministic CBOR presentation envelope with strict-by-default decode
  (round-trip canonical equality), duplicate-key rejection, per-field
  length caps; both ZeroJ and draft-10 sign/verify argument orders.
- BbsZkVerifier integrates with ProofSystemId.BBS for ZkProofEnvelope.
- New zeroj-bbs-wasm scaffold and BbsSelectiveDisclosureExample.

Tests:
- 1,765 zeroj-bls12381 tests including RFC9380 J.9/J.10 vectors,
  ZCash/blst compressed-generator byte vectors, off-curve and torsion
  rejection, oversize-DST handling, Fp2.sqrt(-1) edge case.
- zeroj-bls12381-wasm includes a synthetic-WASM regression test pinning
  the response-buffer dealloc path against malformed-response leaks.
- 100+ byte-level CFRG draft-10 fixture assertions across SHA-256 and
  SHAKE-256: keypair, h2s, MapMessageToScalar, generators, mockedRng,
  20 signature cases, 30 proof cases, run against all three BLS
  providers (pure-Java, wasm-zkcrypto, blst).
- All pre-existing Groth16/PlonK/KZG/MSM/FFT tests pass unchanged after
  the import migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per ADR-0019 §6/§7 amendment: BLS-provider swapping happens in zeroj-bbs via
BbsService.withBlsProvider(...), not in a separate hybrid module. The same
no-extra-module precedent that ADR-0019 set for blst now also covers WASM BLS
primitives.

The standalone WasmBbsProvider that shipped in 0c1d3b1 was redundant — it just
wrapped BbsProviders.withBlsProvider(suite, WasmBls12381Provider.createDefault()).
BbsBlsProviderConformanceTest in zeroj-bbs already runs the full CFRG draft-10
fixture suite (10 signature cases + 15 proof cases + keypair + h2s + generators
+ MapMessageToScalar + mockedRng, both ciphersuites) across all three BLS
providers via withBlsProvider, so the hybrid coverage is preserved without the
typed class.

zeroj-bbs-wasm is now reserved for the full Rust-WASM BBS provider (Phase 3),
where the entire BBS algorithm runs inside WebAssembly via Chicory — Rust
candidate zkryptium gated on wasm32-unknown-unknown compile, no unexpected
host imports, and pinned draft-10 vector equality.

ADR-0019 changes:
- §6: BLS-provider swapping happens in zeroj-bbs (no zeroj-bbs-wasm-bls, no
  zeroj-bbs-blst); conformance suite is the audit
- §7 (was last paragraph of §6): zeroj-bbs-wasm is reserved for full Rust-WASM
  BBS; coarse zeroj_bbs_* ABI; zkryptium candidate; mattrglobal/pairing_crypto
  explicitly rejected (draft-03 / BBS+ flavor, wrong spec)
- §8: renumbered (was §7) — verifier integration
- Implementation Plan #10: rewritten to describe the full Rust-WASM build
- Risks: add "Rust crate pulls in getrandom/wasm-bindgen host shim" row
- References: add pairing_crypto link with explicit non-candidate note

zeroj-bbs-wasm changes:
- Delete WasmBbsProvider.java (the redundant hybrid wrapper)
- Delete WasmBbsProviderTest.java (its coverage is in BbsBlsProviderConformanceTest)
- README rewritten: redirect hybrid users to BbsService.withBlsProvider; document
  the module as reserved for the full Rust-WASM BBS provider
- build.gradle: drop the zeroj-bls12381-wasm dependency (Phase 3 zkryptium will
  bundle BLS12-381 directly); update description

Tests: ./gradlew :zeroj-bbs:test --tests BbsBlsProviderConformanceTest passes
wasm-zkcrypto BLS12381_SHA256, wasm-zkcrypto BLS12381_SHAKE256, blst
BLS12381_SHA256, blst BLS12381_SHAKE256 — proves hybrid coverage survives
the deletion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements ADR-0019 §7 Design B. The entire CFRG BBS draft-10 algorithm
(KeyGen, SkToPk, Sign, Verify, ProofGen, ProofVerify) runs inside
WebAssembly via zkryptium 0.6.1 compiled to wasm32-unknown-unknown,
executed through Chicory 1.7.5. ZeroJ's Java layer serializes requests,
parses responses, and supplies entropy via a single named host import.

ADR-0019 §7 amendment:
- Loosen "no host imports" / "host RNG not permitted" to permit exactly
  one named host import (env.zeroj_host_getrandom) backed by Java
  SecureRandom. Without this, zkryptium's proof_gen cannot run on
  wasm32-unknown-unknown because rand::thread_rng → getrandom needs a
  randomness source. CFRG mockedRng proof byte-equality remains gated
  on the pure-Java provider in BbsBlsProviderConformanceTest; the WASM
  provider verifies proof correctness via roundtrip.

Rust crate (zeroj-bbs-wasm/rust/):
- Cargo.toml: zkryptium 0.6.1 (no_std bbsplus feature only),
  getrandom 0.2 with the "custom" feature so register_custom_getrandom!
  routes through our host import without pulling wasm-bindgen.
- Cargo.lock committed, rust-toolchain.toml pins rustc 1.94.0 +
  wasm32-unknown-unknown target.
- src/lib.rs: 9 ABI exports (zeroj_bbs_version + alloc + dealloc +
  zeroj_bbs_keygen/sk_to_pk/sign/verify/proof_gen/proof_verify) with
  ciphersuite-byte dispatch between Bls12381Sha256 and Bls12381Shake256.
  Response framing is [u32 LE length | status byte | payload], identical
  to zeroj-bls12381-wasm.

Java client + provider (zeroj-bbs-wasm/src/main/java/.../wasm/):
- Bbs12381WasmException, Bbs12381WasmClient, WasmBbsProvider.
- Chicory ImportValues registers env.zeroj_host_getrandom backed by
  java.security.SecureRandom (bounded MAX_HOST_GETRANDOM_LEN = 16 KiB).
- invoke() / invokeNoArg() use the leak-safe pattern from Bls12381WasmClient:
  responseAllocationLen captured before length validation so malformed
  responses still get freed.
- WasmBbsProvider implements BbsProvider; per-ciphersuite instances.

Build wiring:
- build.gradle: buildBbsWasm Exec task invokes cargo via
  ~/.cargo/bin/cargo (overridable via CARGO env var); copyBbsWasm copies
  the .wasm into processResources; Chicory deps added.
- META-INF/native-image/.../resource-config.json whitelists the .wasm for
  GraalVM native-image.
- .gitignore: add zeroj-bbs-wasm/rust/target/.

Tests (zeroj-bbs-wasm/src/test/java/.../wasm/WasmBbsProviderTest):
- wasmModule_hasExactlyOneImportAndExpectedExports: asserts exactly one
  env.zeroj_host_getrandom import and all 9 expected exports.
- keygenAndSkToPk_matchDraft10ShaFixture: byte-exact against
  keypair.json (60e55110…237169fc / a820f230…55ded0c).
- sign_matchesDraft10ShaSingleMessageFixture: byte-exact against
  signature001.json (84773160…7b4565a0).
- verify_rejectsTamperedSignature: bit-flipped signature rejected.
- proofGen_roundtripsViaProofVerify: real-RNG proof_gen → proof_verify
  accepts.
- proofGen_isNonDeterministicAcrossCalls: distinct proofs across calls
  proves host RNG path is wired.
- shake256_signRoundtripsViaVerify: SHAKE-256 ciphersuite covered.
- rawInvocation_reportsTypedExceptionOnShortInput,
  rawInvocation_reportsTypedExceptionOnInvalidPublicKey,
  repeatedErrors_doNotPoisonClient: hardening.
- malformedResponseLength_freesResponseAllocationOnInvoke /
  ...OnInvokeNoArg: synthetic-WASM regression test (mirrors the
  Bls12381WasmClient response-leak fix verified in commit 1b1fa94).

Verification:
- ./gradlew :zeroj-bbs-wasm:test → 12 tests, 0 failures.
- ./gradlew :zeroj-bbs:test :zeroj-bls12381-wasm:test :zeroj-bls12381:test
  build -x test → all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five fixes against Codex's review of commit 268b7d3.

(P1) Per-call SecureRandom now drives the host getrandom import:
- Bbs12381WasmClient.proofGen takes a SecureRandom parameter.
- A volatile proofGenRandom field is swapped in/out around the synchronized
  invoke; the Chicory host function reads it (falling back to defaultRandom
  if null).
- WasmBbsProvider.proofGen threads the SPI-supplied SecureRandom through.
- New test proofGen_honorsPerCallSecureRandom uses a CountingSecureRandom
  to prove the per-call instance is read AND the constructor's defaultRandom
  is NOT read while a per-call random is supplied. Deterministic ops
  (keygen, sk_to_pk, sign) consume zero host RNG bytes, as expected.

(P1) Expanded byte-exact official CFRG draft-10 fixture coverage:
- SHAKE-256 keypair byte-exact (SK + PK).
- SHAKE-256 signature001 (single-message) byte-exact.
- SHAKE-256 signature004 (10-message multi) byte-exact.
- SHA-256 signature004 (10-message multi) byte-exact.
- SHA-256 signature010 (empty header, 10 messages) byte-exact.
- SHA-256 proof001 verification accepts the official mockedRng proof bytes.
- SHAKE-256 proof001 verification accepts the official mockedRng proof bytes.
- Negative test: same proof with a bit-flipped presentation header is rejected.

(P2) Strict boolean response decode:
- decodeBool now rejects any payload byte other than 0x00 or 0x01.
- Synthetic-WASM test verify_rejectsNonCanonicalBooleanResponse drives
  the path: a hand-built module returns status=0 with payload 0x02, and the
  client raises Bbs12381WasmException with "boolean response byte" in the
  message.

(P2) Java-side caps + checked arithmetic:
- MAX_MESSAGES (1024, matches Rust), MAX_MESSAGE_BYTES (64 KiB),
  MAX_HEADER_BYTES (64 KiB), MAX_KEY_INPUT_BYTES (64 KiB), MAX_PROOF_BYTES
  (derived from MAX_MESSAGES), MAX_REQUEST_BYTES (16 MiB).
- validateMessageList computes total payload in long with Math.addExact;
  rejects null entries; caps per-message length.
- validateIndexList caps disclosed-index count and bounds-checks each index.
- requireMaxLength and requireLength replace ad-hoc length checks; null
  inputs are rejected with named errors.
- allocateRequest enforces MAX_REQUEST_BYTES before allocation and downcasts
  via Math.toIntExact.

(P3) ADR-0019 cleanup:
- §7 WASM test plan rewritten: deterministic ops gated byte-for-byte;
  proof_verify gated on official proof bytes; proof_gen gated by roundtrip
  only (host RNG); single named host import asserted in tests.
- Risk table row swapped from "reject candidates that need host RNG" to
  "any imports beyond env.zeroj_host_getrandom rejected by the single-import
  assertion", plus a low-severity row noting per-call SecureRandom can be a
  hardware-backed instance if production requires it.

Verification:
- ./gradlew :zeroj-bbs-wasm:test → 22 tests, 0 failures (was 12).
- ./gradlew :zeroj-bbs:test :zeroj-bls12381-wasm:test :zeroj-bls12381:test
  build -x test → all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… disclosure tests)

Three follow-up fixes against Codex's review of commit 44f969b.

(P1) Per-call SecureRandom now drives the host getrandom import on EVERY
proofGen invocation, not just the first per shared instance:

- Codex's probe found: same WasmBbsProvider used across 5 proofGen calls,
  perCall.bytesRead = [32, 0, 0, 0, 0]. zkryptium's internal
  rand::thread_rng() seeds itself from getrandom once per thread and then
  derives bytes from a cached chacha state, so subsequent calls within the
  same WASM instance bypass the per-call SecureRandom — direct ADR-0019 §7
  violation.

- Fix: Bbs12381WasmClient.proofGen now builds a *fresh* Chicory Instance
  per call via buildInstance(wasmBytes, random). The fresh instance has its
  own thread-local ThreadRng that must re-seed from getrandom on first use,
  which now means the per-call SecureRandom drives entropy for every proof.
  Other ops (keyGen, skToPk, sign, verify, proofVerify) consume no entropy
  and continue to use the persistent instance.

- The volatile proofGenRandom field and invokeWithRandom helper are gone;
  replaced by invokeOnTransientInstance which owns the entire lifecycle of
  one proofGen call.

- Test proofGen_honorsPerCallSecureRandomOnEveryInvocation loops N=5,
  asserting bytesRead > 0 on every call (was bytesRead > 0 on the first call
  only — exactly Codex's probe scenario). defaultRandom asserts == 0 bytes
  across the run.

(P2) Strict-ascending disclosed-index validation at the WASM boundary:

- validateIndexList now rejects non-strictly-ascending input (duplicates,
  reverse order, equal-to-previous). Matches BbsService's contract enforced
  by CfrgBbsCore.validateDisclosedIndexes.

- New validateDisclosedIndexesForVerify (for proofVerify path) checks
  strict-ascending + non-negative without needing the total message count.
  proofVerify additionally asserts disclosedIndexes.length ==
  disclosedMessages.size().

- Tests proofGen_rejectsDuplicateDisclosedIndexes (both {0,0} and {2,0})
  and proofVerify_rejectsDuplicateDisclosedIndexes pin the new validation.

- Previously, direct WasmBbsProvider.proofGen(..., new int[]{0,0}, ...) was
  silently accepted because zkryptium sort+dedups internally.

(P2) Hidden-message selective-disclosure tests:

- proofGen_hiddenMessageSelectiveDisclosureRoundtrip: 10 messages, reveal
  {0, 2, 4, 6} (4 disclosed, 6 hidden), proof_gen + proof_verify roundtrip
  via the WASM provider. Verifies the proof rejects when one disclosed
  message is swapped.

- proofVerify_acceptsOfficialDraft10ShaHiddenMessageProof: byte-compare
  against the official CFRG proof003.json fixture (10 messages, 4 revealed,
  CFRG mockedRng-derived proof bytes). Closes Codex's coverage gap: prior
  proof tests only used SINGLE_MSG with reveal {0} = all-revealed, which
  proved nothing about hidden-message paths.

Verification: ./gradlew :zeroj-bbs-wasm:test → 26 tests, 0 failures (up
from 22). Full regression :zeroj-bbs:test :zeroj-bls12381:test
:zeroj-bls12381-wasm:test --rerun-tasks green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@satran004 satran004 merged commit 12314c4 into main May 16, 2026
9 checks passed
@satran004 satran004 deleted the feat/bbs_wasm_impl branch May 16, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant