feat: extract shared BLS12-381 primitives and add CFRG BBS draft-10#3
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
CFRG BBS (ADR-0019):
Tests: