From 0c1d3b136bdba7b00e6a6a57a8254ad5e6b09cca Mon Sep 17 00:00:00 2001
From: Satya
Date: Sat, 9 May 2026 21:35:35 +0800
Subject: [PATCH 1/6] feat: extract shared BLS12-381 primitives and add CFRG
BBS draft-10
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)
---
.gitignore | 2 +
...d-bls12381-primitives-and-wasm-provider.md | 264 +++++++
...9-cfrg-bbs-pure-java-and-wasm-providers.md | 345 +++++++++
docs/pure-java-prover-guide.md | 3 +-
settings.gradle | 5 +
.../cardano/zeroj/api/ProofSystemId.java | 3 +-
zeroj-bbs-wasm/README.md | 16 +
zeroj-bbs-wasm/build.gradle | 21 +
.../zeroj/bbs/wasm/WasmBbsProvider.java | 132 ++++
.../zeroj/bbs/wasm/WasmBbsProviderTest.java | 82 +++
zeroj-bbs/README.md | 129 ++++
zeroj-bbs/build.gradle | 27 +
.../cardano/zeroj/bbs/BbsCiphersuite.java | 81 +++
.../cardano/zeroj/bbs/BbsException.java | 14 +
.../cardano/zeroj/bbs/BbsKeyPair.java | 16 +
.../cardano/zeroj/bbs/BbsPresentation.java | 31 +
.../zeroj/bbs/BbsPresentationCodec.java | 264 +++++++
.../bloxbean/cardano/zeroj/bbs/BbsProof.java | 46 ++
.../cardano/zeroj/bbs/BbsPublicKey.java | 49 ++
.../cardano/zeroj/bbs/BbsRevealedMessage.java | 41 ++
.../cardano/zeroj/bbs/BbsSecretKey.java | 19 +
.../cardano/zeroj/bbs/BbsService.java | 134 ++++
.../cardano/zeroj/bbs/BbsSignature.java | 43 ++
.../cardano/zeroj/bbs/internal/BbsCodec.java | 298 ++++++++
.../zeroj/bbs/internal/CfrgBbsCore.java | 679 ++++++++++++++++++
.../cardano/zeroj/bbs/spi/BbsProvider.java | 58 ++
.../cardano/zeroj/bbs/spi/BbsProviders.java | 38 +
.../zeroj/bbs/spi/PureJavaBbsProvider.java | 126 ++++
.../zeroj/bbs/verifier/BbsZkVerifier.java | 72 ++
.../zeroj-bbs/reflect-config.json | 13 +
.../zeroj-bbs/resource-config.json | 7 +
...xbean.cardano.zeroj.backend.spi.ZkVerifier | 1 +
.../bbs/BbsBlsProviderConformanceTest.java | 188 +++++
.../cardano/zeroj/bbs/BbsServiceTest.java | 355 +++++++++
.../zeroj/bbs/CfrgBbsDraft10VectorTest.java | 252 +++++++
.../bbs/CfrgBbsOfficialJsonFixtureTest.java | 251 +++++++
.../MapMessageToScalarAsHash.json | 46 ++
.../bls12-381-sha-256/generators.json | 16 +
.../official/bls12-381-sha-256/h2s.json | 6 +
.../official/bls12-381-sha-256/keypair.json | 10 +
.../official/bls12-381-sha-256/mockedRng.json | 18 +
.../bls12-381-sha-256/proof/proof001.json | 34 +
.../bls12-381-sha-256/proof/proof002.json | 52 ++
.../bls12-381-sha-256/proof/proof003.json | 53 ++
.../bls12-381-sha-256/proof/proof004.json | 54 ++
.../bls12-381-sha-256/proof/proof005.json | 54 ++
.../bls12-381-sha-256/proof/proof006.json | 54 ++
.../bls12-381-sha-256/proof/proof007.json | 56 ++
.../bls12-381-sha-256/proof/proof008.json | 56 ++
.../bls12-381-sha-256/proof/proof009.json | 52 ++
.../bls12-381-sha-256/proof/proof010.json | 54 ++
.../bls12-381-sha-256/proof/proof011.json | 56 ++
.../bls12-381-sha-256/proof/proof012.json | 54 ++
.../bls12-381-sha-256/proof/proof013.json | 54 ++
.../bls12-381-sha-256/proof/proof014.json | 53 ++
.../bls12-381-sha-256/proof/proof015.json | 53 ++
.../signature/signature001.json | 19 +
.../signature/signature002.json | 20 +
.../signature/signature003.json | 21 +
.../signature/signature004.json | 28 +
.../signature/signature005.json | 21 +
.../signature/signature006.json | 29 +
.../signature/signature007.json | 29 +
.../signature/signature008.json | 29 +
.../signature/signature009.json | 29 +
.../signature/signature010.json | 28 +
.../MapMessageToScalarAsHash.json | 46 ++
.../bls12-381-shake-256/generators.json | 16 +
.../official/bls12-381-shake-256/h2s.json | 6 +
.../official/bls12-381-shake-256/keypair.json | 10 +
.../bls12-381-shake-256/mockedRng.json | 18 +
.../bls12-381-shake-256/proof/proof001.json | 34 +
.../bls12-381-shake-256/proof/proof002.json | 52 ++
.../bls12-381-shake-256/proof/proof003.json | 53 ++
.../bls12-381-shake-256/proof/proof004.json | 54 ++
.../bls12-381-shake-256/proof/proof005.json | 54 ++
.../bls12-381-shake-256/proof/proof006.json | 54 ++
.../bls12-381-shake-256/proof/proof007.json | 56 ++
.../bls12-381-shake-256/proof/proof008.json | 56 ++
.../bls12-381-shake-256/proof/proof009.json | 52 ++
.../bls12-381-shake-256/proof/proof010.json | 54 ++
.../bls12-381-shake-256/proof/proof011.json | 56 ++
.../bls12-381-shake-256/proof/proof012.json | 54 ++
.../bls12-381-shake-256/proof/proof013.json | 54 ++
.../bls12-381-shake-256/proof/proof014.json | 53 ++
.../bls12-381-shake-256/proof/proof015.json | 53 ++
.../signature/signature001.json | 19 +
.../signature/signature002.json | 20 +
.../signature/signature003.json | 21 +
.../signature/signature004.json | 28 +
.../signature/signature005.json | 21 +
.../signature/signature006.json | 29 +
.../signature/signature007.json | 29 +
.../signature/signature008.json | 29 +
.../signature/signature009.json | 29 +
.../signature/signature010.json | 28 +
.../cfrg-bbs/draft10/official/messages.json | 12 +
.../cfrg-bbs/draft10/sha256.properties | 24 +
zeroj-bls12381-wasm/README.md | 25 +
zeroj-bls12381-wasm/build.gradle | 51 ++
zeroj-bls12381-wasm/rust/Cargo.lock | 105 +++
zeroj-bls12381-wasm/rust/Cargo.toml | 18 +
zeroj-bls12381-wasm/rust/rust-toolchain.toml | 4 +
zeroj-bls12381-wasm/rust/src/lib.rs | 194 +++++
.../bls12381/wasm/Bls12381WasmClient.java | 197 +++++
.../bls12381/wasm/Bls12381WasmException.java | 14 +
.../bls12381/wasm/WasmBls12381Provider.java | 63 ++
.../zeroj-bls12381-wasm/resource-config.json | 9 +
.../bls12381/wasm/Bls12381WasmClientTest.java | 274 +++++++
zeroj-bls12381/build.gradle | 19 +
.../zeroj/bls12381/Bls12381Codecs.java | 293 ++++++++
.../zeroj/bls12381/Bls12381Generators.java | 29 +
.../cardano/zeroj/bls12381/Bls12381Hash.java | 284 ++++++++
.../zeroj/bls12381/Bls12381HashToCurve.java | 306 ++++++++
.../cardano/zeroj/bls12381/ec}/G1Point.java | 39 +-
.../cardano/zeroj/bls12381/ec/G2Point.java | 108 +++
.../zeroj/bls12381}/ec/JacobianG1BLS381.java | 21 +-
.../zeroj/bls12381}/ec/JacobianG2BLS381.java | 17 +-
.../cardano/zeroj}/bls12381/field/Fp.java | 12 +-
.../cardano/zeroj}/bls12381/field/Fp12.java | 2 +-
.../cardano/zeroj}/bls12381/field/Fp2.java | 39 +-
.../cardano/zeroj}/bls12381/field/Fp6.java | 2 +-
.../zeroj/bls12381}/field/MontFp2_381.java | 2 +-
.../zeroj/bls12381}/field/MontFp381.java | 2 +-
.../zeroj/bls12381}/field/MontFr381.java | 2 +-
.../zeroj/bls12381/field/MontUtil.java | 40 ++
.../bls12381/pairing}/BLS12381Pairing.java | 5 +-
.../zeroj/bls12381/spi/Bls12381Provider.java | 168 +++++
.../zeroj/bls12381/spi/Bls12381Providers.java | 12 +
.../spi/PureJavaBls12381Provider.java | 76 ++
.../zeroj-bls12381/reflect-config.json | 110 +++
.../zeroj-bls12381/resource-config.json | 5 +
.../zeroj/bls12381/Bls12381CodecsTest.java | 135 ++++
.../bls12381/Bls12381HashToCurveTest.java | 128 ++++
.../bls12381}/ec/JacobianG1BLS381Test.java | 10 +-
.../bls12381}/ec/JacobianG2BLS381Test.java | 2 +-
.../cardano/zeroj/bls12381/field/Fp2Test.java | 18 +
.../bls12381}/field/MontFp2_381Test.java | 16 +-
.../zeroj/bls12381}/field/MontFp381Test.java | 8 +-
.../zeroj/bls12381}/field/MontFr381Test.java | 2 +-
.../pairing}/BLS12381PairingTest.java | 5 +-
.../spi/PureJavaBls12381ProviderTest.java | 107 +++
zeroj-blst/README.md | 12 +-
zeroj-blst/build.gradle | 1 +
.../zeroj/blst/BlstBls12381Provider.java | 171 +++++
.../cardano/zeroj/blst/package-info.java | 2 +-
.../zeroj-blst/reflect-config.json | 15 +-
.../zeroj/blst/BlstBls12381ProviderTest.java | 100 +++
zeroj-bom/build.gradle | 4 +
zeroj-crypto/build.gradle | 2 +
.../crypto/groth16/Groth16ProofBLS381.java | 4 +-
.../crypto/groth16/Groth16ProverBLS381.java | 10 +-
.../groth16/Groth16ProvingKeyBLS381.java | 4 +-
.../crypto/groth16/ZkeyImporterBLS381.java | 8 +-
.../zeroj/crypto/kzg/KZGCommitmentBLS381.java | 6 +-
.../zeroj/crypto/msm/PippengerBLS381.java | 4 +-
.../zeroj/crypto/plonk/PlonKProofBLS381.java | 2 +-
.../zeroj/crypto/plonk/PlonKProverBLS381.java | 6 +-
.../crypto/plonk/PlonKProvingKeyBLS381.java | 6 +-
.../zeroj/crypto/plonk/PlonKSetupBLS381.java | 4 +-
.../crypto/plonk/PlonKZkeyImporterBLS381.java | 10 +-
.../crypto/plonk/PtauImporterBLS381.java | 8 +-
.../zeroj/crypto/poly/FieldFFTBLS381.java | 2 +-
.../crypto/setup/Groth16SetupBLS381.java | 10 +-
.../zeroj/crypto/setup/PowersOfTauBLS381.java | 10 +-
.../zeroj/crypto/setup/SetupCache.java | 8 +-
.../groth16/Groth16BLS381EndToEndTest.java | 8 +-
.../Groth16BLS381ZkeyEndToEndTest.java | 8 +-
.../crypto/kzg/KZGCommitmentBLS381Test.java | 6 +-
.../zeroj/crypto/msm/PippengerBLS381Test.java | 4 +-
.../crypto/plonk/PlonKBLS381EndToEndTest.java | 10 +-
.../zeroj/crypto/poly/FieldFFTBLS381Test.java | 2 +-
zeroj-examples/build.gradle | 1 +
.../bbs/BbsSelectiveDisclosureExample.java | 74 ++
.../dsl/auction/SealedBidPureJavaE2ETest.java | 8 +-
.../BalanceThresholdPureJavaE2ETest.java | 8 +-
.../ParameterizedCircuitE2ETest.java | 8 +-
.../AnonymousVotingPureJavaE2ETest.java | 8 +-
zeroj-onchain-julc/build.gradle | 5 +-
.../zeroj/onchain/julc/ProverToCardano.java | 4 +-
.../onchain/julc/CircomToOnChainE2ETest.java | 4 +-
.../Groth16BLS12381PureJavaProverTest.java | 4 +-
zeroj-verifier-groth16/build.gradle | 1 +
.../Groth16BLS12381PureJavaVerifier.java | 6 +-
.../groth16/bls12381/field/G2Point.java | 53 --
zeroj-verifier-plonk/build.gradle | 1 +
.../verifier/plonk/PlonkBLS12381Verifier.java | 4 +-
187 files changed, 9835 insertions(+), 196 deletions(-)
create mode 100644 docs/adr/0018-shared-bls12381-primitives-and-wasm-provider.md
create mode 100644 docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
create mode 100644 zeroj-bbs-wasm/README.md
create mode 100644 zeroj-bbs-wasm/build.gradle
create mode 100644 zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
create mode 100644 zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
create mode 100644 zeroj-bbs/README.md
create mode 100644 zeroj-bbs/build.gradle
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsCiphersuite.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsException.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsKeyPair.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentation.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPresentationCodec.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsProof.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsPublicKey.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsRevealedMessage.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSecretKey.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsService.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/BbsSignature.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/BbsCodec.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/internal/CfrgBbsCore.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProvider.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProviders.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/PureJavaBbsProvider.java
create mode 100644 zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/verifier/BbsZkVerifier.java
create mode 100644 zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/reflect-config.json
create mode 100644 zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/resource-config.json
create mode 100644 zeroj-bbs/src/main/resources/META-INF/services/com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier
create mode 100644 zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsBlsProviderConformanceTest.java
create mode 100644 zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsServiceTest.java
create mode 100644 zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsDraft10VectorTest.java
create mode 100644 zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsOfficialJsonFixtureTest.java
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/MapMessageToScalarAsHash.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/generators.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/h2s.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/keypair.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/mockedRng.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof001.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof002.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof003.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof004.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof005.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof006.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof007.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof008.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof009.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof010.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof011.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof012.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof013.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof014.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof015.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature001.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature002.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature003.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature004.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature005.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature006.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature007.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature008.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature009.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature010.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/MapMessageToScalarAsHash.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/generators.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/h2s.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/keypair.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/mockedRng.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof001.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof002.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof003.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof004.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof005.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof006.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof007.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof008.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof009.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof010.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof011.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof012.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof013.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof014.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof015.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature001.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature002.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature003.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature004.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature005.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature006.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature007.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature008.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature009.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature010.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/messages.json
create mode 100644 zeroj-bbs/src/test/resources/cfrg-bbs/draft10/sha256.properties
create mode 100644 zeroj-bls12381-wasm/README.md
create mode 100644 zeroj-bls12381-wasm/build.gradle
create mode 100644 zeroj-bls12381-wasm/rust/Cargo.lock
create mode 100644 zeroj-bls12381-wasm/rust/Cargo.toml
create mode 100644 zeroj-bls12381-wasm/rust/rust-toolchain.toml
create mode 100644 zeroj-bls12381-wasm/rust/src/lib.rs
create mode 100644 zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClient.java
create mode 100644 zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmException.java
create mode 100644 zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/WasmBls12381Provider.java
create mode 100644 zeroj-bls12381-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bls12381-wasm/resource-config.json
create mode 100644 zeroj-bls12381-wasm/src/test/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClientTest.java
create mode 100644 zeroj-bls12381/build.gradle
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Codecs.java
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Generators.java
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Hash.java
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurve.java
rename {zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec}/G1Point.java (64%)
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G2Point.java
rename {zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381}/ec/JacobianG1BLS381.java (93%)
rename {zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381}/ec/JacobianG2BLS381.java (92%)
rename {zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16 => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj}/bls12381/field/Fp.java (70%)
rename {zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16 => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj}/bls12381/field/Fp12.java (96%)
rename {zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16 => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj}/bls12381/field/Fp2.java (52%)
rename {zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16 => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj}/bls12381/field/Fp6.java (96%)
rename {zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381}/field/MontFp2_381.java (98%)
rename {zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381}/field/MontFp381.java (99%)
rename {zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381}/field/MontFr381.java (99%)
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontUtil.java
rename {zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field => zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/pairing}/BLS12381Pairing.java (97%)
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Providers.java
create mode 100644 zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java
create mode 100644 zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/reflect-config.json
create mode 100644 zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/resource-config.json
create mode 100644 zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java
create mode 100644 zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java
rename {zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381}/ec/JacobianG1BLS381Test.java (89%)
rename {zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381}/ec/JacobianG2BLS381Test.java (97%)
create mode 100644 zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2Test.java
rename {zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381}/field/MontFp2_381Test.java (83%)
rename {zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381}/field/MontFp381Test.java (93%)
rename {zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto => zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381}/field/MontFr381Test.java (98%)
rename {zeroj-verifier-groth16/src/test/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381 => zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing}/BLS12381PairingTest.java (94%)
create mode 100644 zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381ProviderTest.java
create mode 100644 zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381Provider.java
create mode 100644 zeroj-blst/src/test/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381ProviderTest.java
create mode 100644 zeroj-examples/src/main/java/com/bloxbean/cardano/zeroj/examples/bbs/BbsSelectiveDisclosureExample.java
delete mode 100644 zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G2Point.java
diff --git a/.gitignore b/.gitignore
index 5fe490f..6dde657 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,8 @@ 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/
### 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..3208ebe
--- /dev/null
+++ b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
@@ -0,0 +1,345 @@
+# 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. Add explicit optional non-default providers
+
+`zeroj-bbs-wasm` implements the same `BbsProvider` SPI as an explicit opt-in.
+The first implementation reuses the vector-compatible Java BBS core and swaps
+the BLS12-381 primitive provider to `zeroj-bls12381-wasm`, which is Rust
+compiled to `wasm32-unknown-unknown` and executed through Chicory 1.7.5.
+
+This keeps one audited BBS algorithm path while still allowing WASM-backed
+pairing and scalar multiplication. A future optimization may add a coarse Rust
+BBS ABI if vector compatibility and performance justify the extra implementation
+surface.
+
+Native `blst` is exposed as a BLS12-381 provider from `zeroj-blst`. BBS can use
+it through explicit BLS provider selection without adding a separate
+`zeroj-bbs-blst` module:
+
+```java
+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 test or user-selected optional
+dependencies.
+
+If a coarse Rust BBS ABI is added later, it should use operations like:
+
+- `zeroj_bbs_keygen`
+- `zeroj_bbs_sk_to_pk`
+- `zeroj_bbs_sign`
+- `zeroj_bbs_verify`
+- `zeroj_bbs_proof_gen`
+- `zeroj_bbs_proof_verify`
+
+The Rust candidate may be `zkryptium` if it compiles cleanly to
+`wasm32-unknown-unknown`, has no unexpected host imports, and passes the pinned
+draft-10 vectors. 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 into a draft-10 shape.
+
+The WASM ABI should 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`
+- no unexpected host imports
+- typed Java exceptions for malformed responses
+- tests for alloc/dealloc balance on error paths
+
+### 7. 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 has no unexpected host imports.
+- All CFRG vectors match the pure Java provider.
+- Malformed requests and malformed responses map to typed Java exceptions.
+- Repeated WASM errors do not leak response buffers.
+- The Rust crate does not call host randomness for deterministic operations;
+ Java supplies randomness or mocked random scalar material.
+
+### 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. Create `zeroj-bbs-wasm` with a Rust candidate gated by draft vectors, no
+ host imports, and hardened memory cleanup tests.
+10. Run the shared conformance suite against pure Java, WASM, and blst BLS
+ providers.
+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 |
+| 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:
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..ad5df25
--- /dev/null
+++ b/zeroj-bbs-wasm/README.md
@@ -0,0 +1,16 @@
+# zeroj-bbs-wasm
+
+Optional CFRG BBS provider backed by `zeroj-bls12381-wasm`.
+
+The BBS draft-10 algorithm is shared with `zeroj-bbs`; this module swaps the
+BLS12-381 primitive provider to the Rust `bls12_381` WASM backend executed by
+Chicory. Provider selection is explicit:
+
+```java
+var provider = com.bloxbean.cardano.zeroj.bbs.wasm.WasmBbsProvider.createDefault();
+var service = new com.bloxbean.cardano.zeroj.bbs.BbsService(provider);
+```
+
+The generated WASM artifact comes from `zeroj-bls12381-wasm` during the Gradle
+build. Install Rust with the pinned toolchain and the `wasm32-unknown-unknown`
+target before running this module's tests.
diff --git a/zeroj-bbs-wasm/build.gradle b/zeroj-bbs-wasm/build.gradle
new file mode 100644
index 0000000..09cc3e3
--- /dev/null
+++ b/zeroj-bbs-wasm/build.gradle
@@ -0,0 +1,21 @@
+plugins {
+ id 'java-library'
+}
+
+description = 'ZeroJ BBS WASM-backed provider using BLS12-381 Rust WASM primitives'
+
+dependencies {
+ api project(':zeroj-bbs')
+ implementation project(':zeroj-bls12381-wasm')
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ pom {
+ name = 'ZeroJ BBS WASM'
+ description = 'Optional CFRG BBS provider using Rust WASM BLS12-381 primitives and Chicory'
+ }
+ }
+ }
+}
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..6b3994c
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
@@ -0,0 +1,132 @@
+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.CfrgBbsCore;
+import com.bloxbean.cardano.zeroj.bbs.spi.BbsProvider;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
+
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * CFRG BBS provider backed by the ZeroJ BLS12-381 Rust WASM provider.
+ */
+public final class WasmBbsProvider implements BbsProvider {
+ private final BbsCiphersuite ciphersuite;
+ private final Bls12381Provider bls;
+
+ public WasmBbsProvider(BbsCiphersuite ciphersuite, Bls12381Provider bls) {
+ this.ciphersuite = Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ this.bls = Objects.requireNonNull(bls, "BLS provider required");
+ }
+
+ public static WasmBbsProvider createDefault() {
+ return new WasmBbsProvider(BbsCiphersuite.BLS12381_SHA256, WasmBls12381Provider.createDefault());
+ }
+
+ @Override
+ public String id() {
+ return "zeroj-bbs-wasm-bls12381-zkcrypto";
+ }
+
+ @Override
+ public BbsCiphersuite ciphersuite() {
+ return ciphersuite;
+ }
+
+ @Override
+ public BbsSecretKey keyGen(byte[] keyMaterial, byte[] keyInfo) {
+ return new BbsSecretKey(CfrgBbsCore.keyGen(ciphersuite, keyMaterial, keyInfo), ciphersuite);
+ }
+
+ @Override
+ public BbsPublicKey skToPk(BbsSecretKey secretKey) {
+ requireSuite(secretKey);
+ return new BbsPublicKey(CfrgBbsCore.skToPk(secretKey.value(), bls), ciphersuite);
+ }
+
+ @Override
+ public BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, List messages, byte[] header) {
+ requireSuite(secretKey);
+ requireSuite(publicKey);
+ return new BbsSignature(CfrgBbsCore.sign(
+ secretKey.value(), publicKey.bytes(), messages, header, ciphersuite, bls), ciphersuite);
+ }
+
+ @Override
+ public boolean verify(BbsPublicKey publicKey, BbsSignature signature, List messages, byte[] header) {
+ if (publicKey.ciphersuite() != ciphersuite || signature.ciphersuite() != ciphersuite) {
+ return false;
+ }
+ return CfrgBbsCore.verify(publicKey.bytes(), signature.bytes(), messages, header, ciphersuite, bls);
+ }
+
+ @Override
+ public BbsProof proofGen(
+ BbsPublicKey publicKey,
+ BbsSignature signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes,
+ SecureRandom random) {
+ requireSuite(publicKey);
+ requireSuite(signature);
+ return new BbsProof(CfrgBbsCore.proofGen(
+ publicKey.bytes(),
+ signature.bytes(),
+ messages,
+ header,
+ presentationHeader,
+ disclosedIndexes,
+ ciphersuite,
+ bls,
+ random), 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 CfrgBbsCore.proofVerify(
+ publicKey.bytes(),
+ proof.bytes(),
+ header,
+ presentationHeader,
+ disclosedMessages,
+ disclosedIndexes,
+ ciphersuite,
+ bls);
+ }
+
+ 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/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..db4c48c
--- /dev/null
+++ b/zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
@@ -0,0 +1,82 @@
+package com.bloxbean.cardano.zeroj.bbs.wasm;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+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.CfrgBbsCore;
+import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WasmBbsProviderTest {
+ private static final BbsCiphersuite SUITE = BbsCiphersuite.BLS12381_SHA256;
+ private static final byte[] KEY_MATERIAL = hex("746869732d49532d6a7573742d616e2d546573742d494b4d2d746f2d67656e65726174652d246528724074232d6b6579");
+ private static final byte[] KEY_INFO = hex("746869732d49532d736f6d652d6b65792d6d657461646174612d746f2d62652d757365642d696e2d746573742d6b65792d67656e");
+ private static final byte[] SK = hex("60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc");
+ private static final byte[] PK = hex("a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c");
+ private static final byte[] HEADER = hex("11223344556677889900aabbccddeeff");
+ private static final byte[] PRESENTATION_HEADER = hex("bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501");
+ private static final byte[] MULTI_SIGNATURE = hex("8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8");
+ private static final byte[] SOME_DISCLOSED_PROOF = hex("a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a");
+
+ @Test
+ void providerMatchesDraft10KeyAndSignatureVectors() {
+ var provider = WasmBbsProvider.createDefault();
+ BbsSecretKey secretKey = provider.keyGen(KEY_MATERIAL, KEY_INFO);
+ BbsPublicKey publicKey = provider.skToPk(secretKey);
+
+ assertArrayEquals(SK, secretKey.toBytes());
+ assertArrayEquals(PK, publicKey.bytes());
+
+ BbsSignature signature = provider.sign(secretKey, publicKey, messages(), HEADER);
+ assertArrayEquals(MULTI_SIGNATURE, signature.bytes());
+ assertTrue(provider.verify(publicKey, signature, messages(), HEADER));
+ }
+
+ @Test
+ void wasmBlsProviderVerifiesOfficialSomeDisclosedProofVector() {
+ var bls = WasmBls12381Provider.createDefault();
+ int[] disclosedIndexes = {0, 2, 4, 6};
+ List disclosedMessages = Arrays.stream(disclosedIndexes)
+ .mapToObj(messages()::get)
+ .toList();
+
+ assertTrue(CfrgBbsCore.proofVerify(
+ PK,
+ SOME_DISCLOSED_PROOF,
+ HEADER,
+ PRESENTATION_HEADER,
+ disclosedMessages,
+ disclosedIndexes,
+ SUITE,
+ bls));
+ }
+
+ private static List messages() {
+ return List.of(
+ hex("9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"),
+ hex("c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"),
+ hex("7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73"),
+ hex("77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c"),
+ hex("496694774c5604ab1b2544eababcf0f53278ff50"),
+ hex("515ae153e22aae04ad16f759e07237b4"),
+ hex("d183ddc6e2665aa4e2f088af"),
+ hex("ac55fb33a75909ed"),
+ hex("96012096"),
+ new byte[0]);
+ }
+
+ 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;
+ }
+}
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.
+ *
+ * Map keys are fixed:
+ *
+ * 1: version (uint)
+ * 2: ciphersuite id (text)
+ * 3: draft BBS proof bytes (bstr)
+ * 4: header (bstr)
+ * 5: presentation header (bstr)
+ * 6: revealed messages ([[index, message], ...])
+ *
+ */
+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 domArray = new ArrayList<>(generators.count() + 1);
+ domArray.add((long) generators.h().size());
+ domArray.add(generators.q1());
+ domArray.addAll(generators.h());
+ byte[] domOcts = BbsCodec.concat(BbsCodec.serialize(domArray.toArray()), ciphersuite.apiId());
+ byte[] domInput = BbsCodec.concat(publicKey, domOcts, BbsCodec.i2osp(header.length, 8), header);
+ return hashToScalar(domInput, dst(ciphersuite.apiId(), "H2S_"), ciphersuite);
+ }
+
+ public static G1Point calculateB(
+ Generators generators,
+ BigInteger domain,
+ List messages,
+ BbsCiphersuite ciphersuite
+ ) {
+ return computeB(generators, domain, messages, ciphersuite);
+ }
+
+ public static byte[] sign(
+ BigInteger secretKey,
+ byte[] publicKey,
+ List messages,
+ byte[] header,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ BbsCodec.requireNonZeroScalar(secretKey, "BBS secret key");
+ BbsCodec.octetsToPublicKey(publicKey);
+ List messageScalars = messagesToScalars(messages, ciphersuite);
+ Generators generators = createGenerators(messageScalars.size() + 1, ciphersuite, bls);
+ BbsCodec.SignatureParts signature = coreSign(
+ secretKey, publicKey, generators, BbsCodec.copy(header), messageScalars, ciphersuite, bls);
+ return BbsCodec.signatureToOctets(signature);
+ }
+
+ public static boolean verify(
+ byte[] publicKey,
+ byte[] signature,
+ List messages,
+ byte[] header,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ try {
+ List messageScalars = messagesToScalars(messages, ciphersuite);
+ Generators generators = createGenerators(messageScalars.size() + 1, ciphersuite, bls);
+ return coreVerify(publicKey, signature, generators, BbsCodec.copy(header), messageScalars, ciphersuite, bls);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ public static byte[] proofGen(
+ byte[] publicKey,
+ byte[] signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls,
+ SecureRandom random
+ ) {
+ Objects.requireNonNull(random, "secure random required");
+ int messageCount = Objects.requireNonNull(messages, "messages required").size();
+ int[] indexes = validateDisclosedIndexes(disclosedIndexes, messageCount);
+ int undisclosedCount = messageCount - indexes.length;
+ return proofGenWithRandomScalars(
+ publicKey,
+ signature,
+ messages,
+ header,
+ presentationHeader,
+ indexes,
+ ciphersuite,
+ bls,
+ randomScalars(5 + undisclosedCount, ciphersuite, random));
+ }
+
+ public static byte[] proofGenWithRandomScalars(
+ byte[] publicKey,
+ byte[] signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls,
+ List randomScalars
+ ) {
+ List messageScalars = messagesToScalars(messages, ciphersuite);
+ int[] indexes = validateDisclosedIndexes(disclosedIndexes, messageScalars.size());
+ Generators generators = createGenerators(messageScalars.size() + 1, ciphersuite, bls);
+ if (!coreVerify(publicKey, signature, generators, BbsCodec.copy(header), messageScalars, ciphersuite, bls, true)) {
+ throw new IllegalArgumentException("Cannot generate BBS proof for an invalid signature");
+ }
+
+ BbsCodec.SignatureParts signatureParts = BbsCodec.octetsToSignature(signature, ciphersuite);
+ int[] undisclosedIndexes = undisclosedIndexes(messageScalars.size(), indexes);
+ List disclosedMessages = select(messageScalars, indexes);
+ List undisclosedMessages = select(messageScalars, undisclosedIndexes);
+ List proofRandomScalars = validateRandomScalars(randomScalars, 5 + undisclosedIndexes.length);
+
+ ProofInitResult initResult = proofInit(
+ publicKey,
+ signatureParts,
+ generators,
+ proofRandomScalars,
+ BbsCodec.copy(header),
+ messageScalars,
+ undisclosedIndexes,
+ ciphersuite,
+ bls);
+ BigInteger challenge = proofChallengeCalculate(
+ initResult,
+ disclosedMessages,
+ indexes,
+ BbsCodec.copy(presentationHeader),
+ ciphersuite);
+ return proofFinalize(initResult, challenge, signatureParts.e(), proofRandomScalars, undisclosedMessages);
+ }
+
+ public static boolean proofVerify(
+ byte[] publicKey,
+ byte[] proof,
+ byte[] header,
+ byte[] presentationHeader,
+ List disclosedMessages,
+ int[] disclosedIndexes,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ try {
+ BbsCodec.ProofParts proofParts = BbsCodec.octetsToProof(proof, ciphersuite);
+ int undisclosedCount = proofParts.mHats().size();
+ int revealedCount = Objects.requireNonNull(disclosedMessages, "disclosed messages required").size();
+ int messageCount = undisclosedCount + revealedCount;
+ int[] indexes = validateDisclosedIndexes(disclosedIndexes, messageCount);
+ if (indexes.length != revealedCount) {
+ return false;
+ }
+ List disclosedScalars = messagesToScalars(disclosedMessages, ciphersuite);
+ Generators generators = createGenerators(messageCount + 1, ciphersuite, bls);
+ return coreProofVerify(
+ publicKey,
+ proofParts,
+ generators,
+ BbsCodec.copy(header),
+ BbsCodec.copy(presentationHeader),
+ disclosedScalars,
+ indexes,
+ ciphersuite,
+ bls);
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ public static List seededRandomScalars(
+ byte[] seed,
+ byte[] dst,
+ int count,
+ BbsCiphersuite ciphersuite
+ ) {
+ Objects.requireNonNull(seed, "seed required");
+ Objects.requireNonNull(dst, "dst required");
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ if (count < 0) {
+ throw new IllegalArgumentException("random scalar count must be non-negative");
+ }
+ int outLen = ciphersuite.expandLen() * count;
+ if (outLen > 65535) {
+ throw new IllegalArgumentException("mocked random scalar output exceeds 65535 bytes");
+ }
+ byte[] uniform = expandMessage(seed, dst, outLen, ciphersuite);
+ List scalars = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ scalars.add(new BigInteger(1, Arrays.copyOfRange(
+ uniform, i * ciphersuite.expandLen(), (i + 1) * ciphersuite.expandLen())).mod(R));
+ }
+ return List.copyOf(scalars);
+ }
+
+ public static BigInteger hashToScalar(byte[] message, byte[] dst, BbsCiphersuite ciphersuite) {
+ Objects.requireNonNull(message, "message required");
+ Objects.requireNonNull(dst, "dst required");
+ byte[] uniform = expandMessage(message, dst, ciphersuite.expandLen(), ciphersuite);
+ return new BigInteger(1, uniform).mod(R);
+ }
+
+ public static List randomScalars(int count, BbsCiphersuite ciphersuite, SecureRandom random) {
+ if (count < 0) {
+ throw new IllegalArgumentException("random scalar count must be non-negative");
+ }
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(random, "secure random required");
+ byte[] buffer = new byte[ciphersuite.expandLen()];
+ List scalars = new ArrayList<>(count);
+ while (scalars.size() < count) {
+ random.nextBytes(buffer);
+ BigInteger scalar = new BigInteger(1, buffer).mod(R);
+ if (scalar.signum() != 0) {
+ scalars.add(scalar);
+ }
+ }
+ return List.copyOf(scalars);
+ }
+
+ private static BbsCodec.SignatureParts coreSign(
+ BigInteger secretKey,
+ byte[] publicKey,
+ Generators generators,
+ byte[] header,
+ List messageScalars,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ BigInteger domain = calculateDomain(publicKey, generators, header, ciphersuite);
+ List eInput = new ArrayList<>(messageScalars.size() + 2);
+ eInput.add(secretKey);
+ eInput.addAll(messageScalars);
+ eInput.add(domain);
+ BigInteger e = hashToScalar(BbsCodec.serialize(eInput.toArray()), dst(ciphersuite.apiId(), "H2S_"), ciphersuite);
+ BbsCodec.requireNonZeroScalar(e, "signature e");
+
+ BigInteger denominator = mod(secretKey.add(e));
+ if (denominator.signum() == 0) {
+ throw new IllegalArgumentException("BBS signature denominator is zero");
+ }
+ G1Point b = computeBSecretMessages(generators, domain, messageScalars, ciphersuite, bls);
+ G1Point a = bls.g1SecretScalarMul(b, secretScalarInverse(denominator));
+ return new BbsCodec.SignatureParts(a, e);
+ }
+
+ private static boolean coreVerify(
+ byte[] publicKey,
+ byte[] signature,
+ Generators generators,
+ byte[] header,
+ List messageScalars,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ return coreVerify(publicKey, signature, generators, header, messageScalars, ciphersuite, bls, false);
+ }
+
+ private static boolean coreVerify(
+ byte[] publicKey,
+ byte[] signature,
+ Generators generators,
+ byte[] header,
+ List messageScalars,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls,
+ boolean secretMessages
+ ) {
+ try {
+ BbsCodec.SignatureParts signatureParts = BbsCodec.octetsToSignature(signature, ciphersuite);
+ G2Point w = BbsCodec.octetsToPublicKey(publicKey);
+ BigInteger domain = calculateDomain(publicKey, generators, header, ciphersuite);
+ G1Point b = secretMessages
+ ? computeBSecretMessages(generators, domain, messageScalars, ciphersuite, bls)
+ : computeBPublicMessages(generators, domain, messageScalars, ciphersuite, bls);
+ G1Point left2 = bls.g1Add(bls.g1ScalarMul(signatureParts.a(), signatureParts.e()), bls.g1Negate(b));
+ return bls.pairingProductIsIdentity(
+ new G1Point[]{signatureParts.a(), left2},
+ new G2Point[]{w, bls.g2Generator()});
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ private static ProofInitResult proofInit(
+ byte[] publicKey,
+ BbsCodec.SignatureParts signature,
+ Generators generators,
+ List randomScalars,
+ byte[] header,
+ List messages,
+ int[] undisclosedIndexes,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ BigInteger r1 = randomScalars.get(0);
+ BigInteger r2 = randomScalars.get(1);
+ BigInteger eTilde = randomScalars.get(2);
+ BigInteger r1Tilde = randomScalars.get(3);
+ BigInteger r3Tilde = randomScalars.get(4);
+
+ BigInteger domain = calculateDomain(publicKey, generators, header, ciphersuite);
+ G1Point b = computeBSecretMessages(generators, domain, messages, ciphersuite, bls);
+ G1Point d = bls.g1SecretScalarMul(b, r2);
+ G1Point aBar = bls.g1SecretScalarMul(signature.a(), mod(r1.multiply(r2)));
+ G1Point bBar = bls.g1Add(
+ bls.g1SecretScalarMul(d, r1),
+ bls.g1Negate(bls.g1ScalarMul(aBar, signature.e())));
+ G1Point t1 = bls.g1Add(bls.g1SecretScalarMul(aBar, eTilde), bls.g1SecretScalarMul(d, r1Tilde));
+ G1Point t2 = bls.g1SecretScalarMul(d, r3Tilde);
+ for (int u = 0; u < undisclosedIndexes.length; u++) {
+ t2 = bls.g1Add(
+ t2,
+ bls.g1SecretScalarMul(generators.h().get(undisclosedIndexes[u]), randomScalars.get(5 + u)));
+ }
+ return new ProofInitResult(aBar, bBar, d, t1, t2, domain);
+ }
+
+ private static byte[] proofFinalize(
+ ProofInitResult initResult,
+ BigInteger challenge,
+ BigInteger e,
+ List randomScalars,
+ List undisclosedMessages
+ ) {
+ BigInteger r1 = randomScalars.get(0);
+ BigInteger r2 = randomScalars.get(1);
+ BigInteger eTilde = randomScalars.get(2);
+ BigInteger r1Tilde = randomScalars.get(3);
+ BigInteger r3Tilde = randomScalars.get(4);
+ BigInteger r3 = secretScalarInverse(r2);
+
+ BigInteger eHat = mod(eTilde.add(e.multiply(challenge)));
+ BigInteger r1Hat = mod(r1Tilde.subtract(r1.multiply(challenge)));
+ BigInteger r3Hat = mod(r3Tilde.subtract(r3.multiply(challenge)));
+ List mHats = new ArrayList<>(undisclosedMessages.size());
+ for (int i = 0; i < undisclosedMessages.size(); i++) {
+ mHats.add(mod(randomScalars.get(5 + i).add(undisclosedMessages.get(i).multiply(challenge))));
+ }
+
+ return BbsCodec.proofToOctets(new BbsCodec.ProofParts(
+ initResult.aBar(),
+ initResult.bBar(),
+ initResult.d(),
+ eHat,
+ r1Hat,
+ r3Hat,
+ mHats,
+ challenge));
+ }
+
+ private static boolean coreProofVerify(
+ byte[] publicKey,
+ BbsCodec.ProofParts proof,
+ Generators generators,
+ byte[] header,
+ byte[] presentationHeader,
+ List disclosedMessages,
+ int[] disclosedIndexes,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ G2Point w = BbsCodec.octetsToPublicKey(publicKey);
+ ProofInitResult initResult = proofVerifyInit(
+ publicKey, proof, generators, header, disclosedMessages, disclosedIndexes, ciphersuite, bls);
+ BigInteger challenge = proofChallengeCalculate(
+ initResult, disclosedMessages, disclosedIndexes, presentationHeader, ciphersuite);
+ if (!proof.challenge().equals(challenge)) {
+ return false;
+ }
+ return bls.pairingProductIsIdentity(
+ new G1Point[]{proof.aBar(), proof.bBar()},
+ new G2Point[]{w, bls.g2Generator().negate()});
+ }
+
+ private static ProofInitResult proofVerifyInit(
+ byte[] publicKey,
+ BbsCodec.ProofParts proof,
+ Generators generators,
+ byte[] header,
+ List disclosedMessages,
+ int[] disclosedIndexes,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ int hiddenCount = proof.mHats().size();
+ int messageCount = hiddenCount + disclosedIndexes.length;
+ int[] undisclosedIndexes = undisclosedIndexes(messageCount, disclosedIndexes);
+ BigInteger domain = calculateDomain(publicKey, generators, header, ciphersuite);
+
+ G1Point t1 = bls.g1Add(
+ bls.g1Add(
+ bls.g1ScalarMul(proof.bBar(), proof.challenge()),
+ bls.g1ScalarMul(proof.aBar(), proof.eHat())),
+ bls.g1ScalarMul(proof.d(), proof.r1Hat()));
+
+ G1Point bv = bls.g1Add(ciphersuite.p1(), bls.g1ScalarMul(generators.q1(), domain));
+ for (int i = 0; i < disclosedIndexes.length; i++) {
+ bv = bls.g1Add(bv, bls.g1ScalarMul(generators.h().get(disclosedIndexes[i]), disclosedMessages.get(i)));
+ }
+
+ G1Point t2 = bls.g1Add(bls.g1ScalarMul(bv, proof.challenge()), bls.g1ScalarMul(proof.d(), proof.r3Hat()));
+ for (int u = 0; u < undisclosedIndexes.length; u++) {
+ t2 = bls.g1Add(t2, bls.g1ScalarMul(generators.h().get(undisclosedIndexes[u]), proof.mHats().get(u)));
+ }
+ return new ProofInitResult(proof.aBar(), proof.bBar(), proof.d(), t1, t2, domain);
+ }
+
+ private static BigInteger proofChallengeCalculate(
+ ProofInitResult initResult,
+ List disclosedMessages,
+ int[] disclosedIndexes,
+ byte[] presentationHeader,
+ BbsCiphersuite ciphersuite
+ ) {
+ if (disclosedMessages.size() != disclosedIndexes.length) {
+ throw new IllegalArgumentException("Disclosed message count must match disclosed index count");
+ }
+ List values = new ArrayList<>(1 + disclosedIndexes.length * 2 + 6);
+ values.add((long) disclosedIndexes.length);
+ for (int i = 0; i < disclosedIndexes.length; i++) {
+ values.add((long) disclosedIndexes[i]);
+ values.add(disclosedMessages.get(i));
+ }
+ values.add(initResult.aBar());
+ values.add(initResult.bBar());
+ values.add(initResult.d());
+ values.add(initResult.t1());
+ values.add(initResult.t2());
+ values.add(initResult.domain());
+
+ byte[] cOctets = BbsCodec.concat(
+ BbsCodec.serialize(values.toArray()),
+ BbsCodec.i2osp(presentationHeader.length, 8),
+ presentationHeader);
+ return hashToScalar(cOctets, dst(ciphersuite.apiId(), "H2S_"), ciphersuite);
+ }
+
+ private static G1Point computeB(
+ Generators generators,
+ BigInteger domain,
+ List messages,
+ BbsCiphersuite ciphersuite
+ ) {
+ return computeB(generators, domain, messages, ciphersuite, null, false);
+ }
+
+ private static G1Point computeBPublicMessages(
+ Generators generators,
+ BigInteger domain,
+ List messages,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ return computeB(generators, domain, messages, ciphersuite, bls, false);
+ }
+
+ private static G1Point computeBSecretMessages(
+ Generators generators,
+ BigInteger domain,
+ List messages,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls
+ ) {
+ return computeB(generators, domain, messages, ciphersuite, bls, true);
+ }
+
+ private static G1Point computeB(
+ Generators generators,
+ BigInteger domain,
+ List messages,
+ BbsCiphersuite ciphersuite,
+ Bls12381Provider bls,
+ boolean secretMessages
+ ) {
+ if (messages.size() != generators.h().size()) {
+ throw new IllegalArgumentException("Message count must match BBS message generator count");
+ }
+ G1Point b = bls == null
+ ? ciphersuite.p1().add(generators.q1().scalarMul(domain))
+ : bls.g1Add(ciphersuite.p1(), bls.g1ScalarMul(generators.q1(), domain));
+ for (int i = 0; i < messages.size(); i++) {
+ G1Point term = bls == null
+ ? generators.h().get(i).scalarMul(messages.get(i))
+ : secretMessages
+ ? bls.g1SecretScalarMul(generators.h().get(i), messages.get(i))
+ : bls.g1ScalarMul(generators.h().get(i), messages.get(i));
+ b = bls == null ? b.add(term) : bls.g1Add(b, term);
+ }
+ return b;
+ }
+
+ private static List validateRandomScalars(List randomScalars, int expected) {
+ Objects.requireNonNull(randomScalars, "random scalars required");
+ if (randomScalars.size() != expected) {
+ throw new IllegalArgumentException("Expected " + expected + " random scalars, got " + randomScalars.size());
+ }
+ List scalars = new ArrayList<>(expected);
+ for (BigInteger scalar : randomScalars) {
+ scalars.add(BbsCodec.requireNonZeroScalar(scalar, "proof random scalar"));
+ }
+ return List.copyOf(scalars);
+ }
+
+ public static int[] validateDisclosedIndexes(int[] disclosedIndexes, int messageCount) {
+ Objects.requireNonNull(disclosedIndexes, "disclosed indexes required");
+ if (messageCount < 0) {
+ throw new IllegalArgumentException("message count must be non-negative");
+ }
+ int[] copy = disclosedIndexes.clone();
+ int previous = -1;
+ for (int index : copy) {
+ if (index < 0 || index >= messageCount) {
+ throw new IllegalArgumentException("BBS disclosed index out of range: " + index);
+ }
+ if (index <= previous) {
+ throw new IllegalArgumentException("BBS disclosed indexes must be strictly ascending");
+ }
+ previous = index;
+ }
+ return copy;
+ }
+
+ private static int[] undisclosedIndexes(int messageCount, int[] disclosedIndexes) {
+ int[] out = new int[messageCount - disclosedIndexes.length];
+ int disclosedOffset = 0;
+ int outOffset = 0;
+ for (int i = 0; i < messageCount; i++) {
+ if (disclosedOffset < disclosedIndexes.length && disclosedIndexes[disclosedOffset] == i) {
+ disclosedOffset++;
+ } else {
+ out[outOffset++] = i;
+ }
+ }
+ return out;
+ }
+
+ private static List select(List values, int[] indexes) {
+ List selected = new ArrayList<>(indexes.length);
+ for (int index : indexes) {
+ selected.add(values.get(index));
+ }
+ return List.copyOf(selected);
+ }
+
+ private static BigInteger mod(BigInteger value) {
+ BigInteger out = value.mod(R);
+ return out.signum() >= 0 ? out : out.add(R);
+ }
+
+ private static BigInteger secretScalarInverse(BigInteger scalar) {
+ BbsCodec.requireNonZeroScalar(scalar, "secret scalar");
+ return MontFr381.fromBigInteger(scalar).inverse().toBigInteger();
+ }
+
+ private static byte[] expandMessage(byte[] message, byte[] dst, int len, BbsCiphersuite ciphersuite) {
+ if (dst.length > 255) {
+ throw new IllegalArgumentException("BBS DST must be at most 255 bytes");
+ }
+ return switch (ciphersuite) {
+ case BLS12381_SHA256 -> Bls12381Hash.expandMessageXmdSha256(message, dst, len);
+ case BLS12381_SHAKE256 -> Bls12381Hash.expandMessageXofShake256(message, dst, len);
+ };
+ }
+
+ private static G1Point hashToG1(byte[] message, byte[] dst, BbsCiphersuite ciphersuite, Bls12381Provider bls) {
+ return switch (ciphersuite) {
+ case BLS12381_SHA256 -> bls.g1HashToCurve(message, dst);
+ case BLS12381_SHAKE256 -> bls.g1HashToCurveXofShake256(message, dst);
+ };
+ }
+
+ private static byte[] dst(byte[] prefix, String suffix) {
+ return BbsCodec.concat(prefix, suffix.getBytes(StandardCharsets.US_ASCII));
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProvider.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProvider.java
new file mode 100644
index 0000000..2a82dc0
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProvider.java
@@ -0,0 +1,58 @@
+package com.bloxbean.cardano.zeroj.bbs.spi;
+
+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 java.security.SecureRandom;
+import java.util.List;
+
+/**
+ * Provider boundary for CFRG BBS draft-10 implementations.
+ */
+public interface BbsProvider {
+ String id();
+
+ BbsCiphersuite ciphersuite();
+
+ BbsSecretKey keyGen(byte[] keyMaterial, byte[] keyInfo);
+
+ BbsPublicKey skToPk(BbsSecretKey secretKey);
+
+ default BbsKeyPair keyPair(byte[] keyMaterial, byte[] keyInfo) {
+ BbsSecretKey secretKey = keyGen(keyMaterial, keyInfo);
+ return new BbsKeyPair(secretKey, skToPk(secretKey));
+ }
+
+ BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, List messages, byte[] header);
+
+ default BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, byte[] header, List messages) {
+ return sign(secretKey, publicKey, messages, header);
+ }
+
+ boolean verify(BbsPublicKey publicKey, BbsSignature signature, List messages, byte[] header);
+
+ default boolean verify(BbsPublicKey publicKey, BbsSignature signature, byte[] header, List messages) {
+ return verify(publicKey, signature, messages, header);
+ }
+
+ BbsProof proofGen(
+ BbsPublicKey publicKey,
+ BbsSignature signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes,
+ SecureRandom random);
+
+ boolean proofVerify(
+ BbsPublicKey publicKey,
+ BbsProof proof,
+ byte[] header,
+ byte[] presentationHeader,
+ List disclosedMessages,
+ int[] disclosedIndexes);
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProviders.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProviders.java
new file mode 100644
index 0000000..dc45885
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/BbsProviders.java
@@ -0,0 +1,38 @@
+package com.bloxbean.cardano.zeroj.bbs.spi;
+
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+
+import java.util.Objects;
+
+/**
+ * Built-in BBS provider factories.
+ */
+public final class BbsProviders {
+ private static final PureJavaBbsProvider SHA256 = new PureJavaBbsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ Bls12381Providers.pureJava());
+ private static final PureJavaBbsProvider SHAKE256 = new PureJavaBbsProvider(
+ BbsCiphersuite.BLS12381_SHAKE256,
+ Bls12381Providers.pureJava());
+
+ private BbsProviders() {}
+
+ public static BbsProvider pureJava() {
+ return SHA256;
+ }
+
+ public static BbsProvider pureJava(BbsCiphersuite ciphersuite) {
+ return switch (Objects.requireNonNull(ciphersuite, "ciphersuite required")) {
+ case BLS12381_SHA256 -> SHA256;
+ case BLS12381_SHAKE256 -> SHAKE256;
+ };
+ }
+
+ public static BbsProvider withBlsProvider(BbsCiphersuite ciphersuite, Bls12381Provider bls) {
+ return new PureJavaBbsProvider(
+ Objects.requireNonNull(ciphersuite, "ciphersuite required"),
+ Objects.requireNonNull(bls, "BLS provider required"));
+ }
+}
diff --git a/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/PureJavaBbsProvider.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/PureJavaBbsProvider.java
new file mode 100644
index 0000000..a7ec8e3
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/spi/PureJavaBbsProvider.java
@@ -0,0 +1,126 @@
+package com.bloxbean.cardano.zeroj.bbs.spi;
+
+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.CfrgBbsCore;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Pure Java CFRG BBS draft-10 provider backed by ZeroJ BLS12-381 primitives.
+ */
+public final class PureJavaBbsProvider implements BbsProvider {
+ private final BbsCiphersuite ciphersuite;
+ private final Bls12381Provider bls;
+
+ public PureJavaBbsProvider(BbsCiphersuite ciphersuite, Bls12381Provider bls) {
+ this.ciphersuite = Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ this.bls = Objects.requireNonNull(bls, "BLS provider required");
+ }
+
+ @Override
+ public String id() {
+ return "zeroj-bbs-pure-java-" + ciphersuite.name().toLowerCase(java.util.Locale.ROOT);
+ }
+
+ @Override
+ public BbsCiphersuite ciphersuite() {
+ return ciphersuite;
+ }
+
+ @Override
+ public BbsSecretKey keyGen(byte[] keyMaterial, byte[] keyInfo) {
+ return new BbsSecretKey(CfrgBbsCore.keyGen(ciphersuite, keyMaterial, keyInfo), ciphersuite);
+ }
+
+ @Override
+ public BbsPublicKey skToPk(BbsSecretKey secretKey) {
+ requireSuite(secretKey);
+ return new BbsPublicKey(CfrgBbsCore.skToPk(secretKey.value(), bls), ciphersuite);
+ }
+
+ @Override
+ public BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, List messages, byte[] header) {
+ requireSuite(secretKey);
+ requireSuite(publicKey);
+ return new BbsSignature(CfrgBbsCore.sign(
+ secretKey.value(), publicKey.bytes(), messages, header, ciphersuite, bls), ciphersuite);
+ }
+
+ @Override
+ public boolean verify(BbsPublicKey publicKey, BbsSignature signature, List messages, byte[] header) {
+ if (publicKey.ciphersuite() != ciphersuite || signature.ciphersuite() != ciphersuite) {
+ return false;
+ }
+ return CfrgBbsCore.verify(publicKey.bytes(), signature.bytes(), messages, header, ciphersuite, bls);
+ }
+
+ @Override
+ public BbsProof proofGen(
+ BbsPublicKey publicKey,
+ BbsSignature signature,
+ List messages,
+ byte[] header,
+ byte[] presentationHeader,
+ int[] disclosedIndexes,
+ SecureRandom random) {
+ requireSuite(publicKey);
+ requireSuite(signature);
+ return new BbsProof(CfrgBbsCore.proofGen(
+ publicKey.bytes(),
+ signature.bytes(),
+ messages,
+ header,
+ presentationHeader,
+ disclosedIndexes,
+ ciphersuite,
+ bls,
+ random), 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 CfrgBbsCore.proofVerify(
+ publicKey.bytes(),
+ proof.bytes(),
+ header,
+ presentationHeader,
+ disclosedMessages,
+ disclosedIndexes,
+ ciphersuite,
+ bls);
+ }
+
+ 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/src/main/java/com/bloxbean/cardano/zeroj/bbs/verifier/BbsZkVerifier.java b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/verifier/BbsZkVerifier.java
new file mode 100644
index 0000000..5b7fb95
--- /dev/null
+++ b/zeroj-bbs/src/main/java/com/bloxbean/cardano/zeroj/bbs/verifier/BbsZkVerifier.java
@@ -0,0 +1,72 @@
+package com.bloxbean.cardano.zeroj.bbs.verifier;
+
+import com.bloxbean.cardano.zeroj.api.CurveId;
+import com.bloxbean.cardano.zeroj.api.ProofSystemId;
+import com.bloxbean.cardano.zeroj.api.VerificationMaterial;
+import com.bloxbean.cardano.zeroj.api.VerificationResult;
+import com.bloxbean.cardano.zeroj.api.ZkProofEnvelope;
+import com.bloxbean.cardano.zeroj.backend.spi.BackendDescriptor;
+import com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier;
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bbs.BbsPresentationCodec;
+import com.bloxbean.cardano.zeroj.bbs.BbsPublicKey;
+import com.bloxbean.cardano.zeroj.bbs.BbsService;
+
+/**
+ * ZeroJ verifier adapter for CFRG BBS draft-10 presentations.
+ */
+public final class BbsZkVerifier implements ZkVerifier {
+ private static final BackendDescriptor DESCRIPTOR =
+ new BackendDescriptor(ProofSystemId.BBS, CurveId.BLS12_381, "bbs-bls12381-java");
+
+ private final BbsService service;
+
+ public BbsZkVerifier() {
+ this(BbsService.pureJava());
+ }
+
+ public BbsZkVerifier(BbsService service) {
+ this.service = java.util.Objects.requireNonNull(service, "BBS service required");
+ }
+
+ @Override
+ public VerificationResult verify(ZkProofEnvelope envelope, VerificationMaterial material) {
+ try {
+ if (envelope.proofSystem() != ProofSystemId.BBS || material.proofSystemId() != ProofSystemId.BBS) {
+ return VerificationResult.error(
+ VerificationResult.ReasonCode.UNSUPPORTED_PROOF_SYSTEM,
+ "BBS verifier only supports proof system bbs");
+ }
+ if (envelope.curve() != CurveId.BLS12_381 || material.curveId() != CurveId.BLS12_381) {
+ return VerificationResult.error(
+ VerificationResult.ReasonCode.UNSUPPORTED_CURVE,
+ "BBS verifier only supports BLS12-381");
+ }
+ if (envelope.proofFormat().isPresent()
+ && !BbsCiphersuite.DEFAULT_PROOF_FORMAT.equals(envelope.proofFormat().get())) {
+ return VerificationResult.error(
+ VerificationResult.ReasonCode.MALFORMED_ENVELOPE,
+ "Unsupported BBS proof format: " + envelope.proofFormat().get());
+ }
+
+ var presentation = BbsPresentationCodec.decode(envelope.proofBytes());
+ BbsCiphersuite ciphersuite = presentation.proof().ciphersuite();
+ BbsService verifierService = service.provider().ciphersuite() == ciphersuite
+ ? service
+ : BbsService.pureJava(ciphersuite);
+ BbsPublicKey publicKey = new BbsPublicKey(material.vkBytes(), ciphersuite);
+ boolean valid = verifierService.verifyPresentation(publicKey, presentation);
+ return valid ? VerificationResult.cryptoValid()
+ : VerificationResult.proofInvalid("BBS proof verification failed");
+ } catch (Exception e) {
+ return VerificationResult.error(
+ VerificationResult.ReasonCode.INTERNAL_ERROR,
+ "BBS verification error: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public BackendDescriptor descriptor() {
+ return DESCRIPTOR;
+ }
+}
diff --git a/zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/reflect-config.json b/zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/reflect-config.json
new file mode 100644
index 0000000..3a739d1
--- /dev/null
+++ b/zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/reflect-config.json
@@ -0,0 +1,13 @@
+[
+ {
+ "name": "com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bbs.verifier.BbsZkVerifier",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true
+ }
+]
diff --git a/zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/resource-config.json b/zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/resource-config.json
new file mode 100644
index 0000000..2cd94b3
--- /dev/null
+++ b/zeroj-bbs/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bbs/resource-config.json
@@ -0,0 +1,7 @@
+{
+ "resources": {
+ "includes": [
+ {"pattern": "META-INF/services/.*"}
+ ]
+ }
+}
diff --git a/zeroj-bbs/src/main/resources/META-INF/services/com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier b/zeroj-bbs/src/main/resources/META-INF/services/com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier
new file mode 100644
index 0000000..98d3bf1
--- /dev/null
+++ b/zeroj-bbs/src/main/resources/META-INF/services/com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier
@@ -0,0 +1 @@
+com.bloxbean.cardano.zeroj.bbs.verifier.BbsZkVerifier
diff --git a/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsBlsProviderConformanceTest.java b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsBlsProviderConformanceTest.java
new file mode 100644
index 0000000..2b05e4b
--- /dev/null
+++ b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsBlsProviderConformanceTest.java
@@ -0,0 +1,188 @@
+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 com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
+import com.bloxbean.cardano.zeroj.blst.BlstBls12381Provider;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class BbsBlsProviderConformanceTest {
+ private static final ObjectMapper JSON = new ObjectMapper();
+ private static final List SUITES = List.of(
+ new SuiteFixture("/cfrg-bbs/draft10/official/bls12-381-sha-256", BbsCiphersuite.BLS12381_SHA256),
+ new SuiteFixture("/cfrg-bbs/draft10/official/bls12-381-shake-256", BbsCiphersuite.BLS12381_SHAKE256));
+
+ @ParameterizedTest(name = "{0} {1}")
+ @MethodSource("providerSuites")
+ void selectedBlsProviderMatchesOfficialKeySignatureAndProofVectors(
+ ProviderFixture providerFixture,
+ SuiteFixture suite) {
+ BbsProvider provider = BbsProviders.withBlsProvider(suite.ciphersuite(), providerFixture.bls());
+
+ assertKeyPairFixture(provider, suite);
+ assertSignatureFixture(provider, suite);
+ assertProofFixture(providerFixture.bls(), provider, suite);
+ }
+
+ private static void assertKeyPairFixture(BbsProvider provider, SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/keypair.json");
+
+ BbsSecretKey secretKey = provider.keyGen(hex(fixture, "keyMaterial"), hex(fixture, "keyInfo"));
+ BbsPublicKey publicKey = provider.skToPk(secretKey);
+
+ assertArrayEquals(hex(fixture.at("/keyPair/secretKey")), secretKey.toBytes());
+ assertArrayEquals(hex(fixture.at("/keyPair/publicKey")), publicKey.bytes());
+ }
+
+ private static void assertSignatureFixture(BbsProvider provider, SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/signature/signature001.json");
+ BbsSecretKey secretKey = new BbsSecretKey(
+ new BigInteger(1, hex(fixture.at("/signerKeyPair/secretKey"))),
+ suite.ciphersuite());
+ BbsPublicKey publicKey = new BbsPublicKey(hex(fixture.at("/signerKeyPair/publicKey")), suite.ciphersuite());
+ BbsSignature expected = new BbsSignature(hex(fixture, "signature"), suite.ciphersuite());
+ List messages = byteArrayList(fixture.get("messages"));
+ byte[] header = hex(fixture, "header");
+
+ BbsSignature generated = provider.sign(secretKey, publicKey, messages, header);
+
+ assertArrayEquals(expected.bytes(), generated.bytes());
+ assertTrue(provider.verify(publicKey, expected, messages, header));
+ assertTrue(provider.verify(publicKey, generated, messages, header));
+ }
+
+ private static void assertProofFixture(Bls12381Provider bls, BbsProvider provider, SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/proof/proof001.json");
+ BbsPublicKey publicKey = new BbsPublicKey(hex(fixture, "signerPublicKey"), suite.ciphersuite());
+ BbsProof expected = new BbsProof(hex(fixture, "proof"), suite.ciphersuite());
+ byte[] header = hex(fixture, "header");
+ byte[] presentationHeader = hex(fixture, "presentationHeader");
+ List messages = byteArrayList(fixture.get("messages"));
+ int[] disclosedIndexes = intArray(fixture.get("disclosedIndexes"));
+ List disclosedMessages = select(messages, disclosedIndexes);
+
+ assertTrue(provider.proofVerify(
+ publicKey,
+ expected,
+ header,
+ presentationHeader,
+ disclosedMessages,
+ disclosedIndexes));
+
+ byte[] generated = CfrgBbsCore.proofGenWithRandomScalars(
+ publicKey.bytes(),
+ hex(fixture, "signature"),
+ messages,
+ header,
+ presentationHeader,
+ disclosedIndexes,
+ suite.ciphersuite(),
+ bls,
+ proofRandomScalars(fixture.at("/trace/random_scalars")));
+ assertArrayEquals(expected.bytes(), generated);
+ }
+
+ private static List proofRandomScalars(JsonNode randomScalars) {
+ List out = new ArrayList<>();
+ out.add(scalar(randomScalars, "r1"));
+ out.add(scalar(randomScalars, "r2"));
+ out.add(scalar(randomScalars, "e_tilde"));
+ out.add(scalar(randomScalars, "r1_tilde"));
+ out.add(scalar(randomScalars, "r3_tilde"));
+ for (JsonNode mTilde : randomScalars.get("m_tilde_scalars")) {
+ out.add(new BigInteger(1, hex(mTilde)));
+ }
+ return List.copyOf(out);
+ }
+
+ private static BigInteger scalar(JsonNode node, String fieldName) {
+ return new BigInteger(1, hex(node, fieldName));
+ }
+
+ private static List select(List values, int[] indexes) {
+ List out = new ArrayList<>(indexes.length);
+ for (int index : indexes) {
+ out.add(values.get(index));
+ }
+ return List.copyOf(out);
+ }
+
+ private static List byteArrayList(JsonNode array) {
+ List out = new ArrayList<>();
+ for (JsonNode item : array) {
+ out.add(hex(item));
+ }
+ return List.copyOf(out);
+ }
+
+ private static int[] intArray(JsonNode array) {
+ int[] out = new int[array.size()];
+ for (int i = 0; i < array.size(); i++) {
+ out[i] = array.get(i).asInt();
+ }
+ return out;
+ }
+
+ private static byte[] hex(JsonNode node, String fieldName) {
+ return hex(node.get(fieldName));
+ }
+
+ private static byte[] hex(JsonNode node) {
+ String hex = node.asText();
+ if (hex.isEmpty()) {
+ return new byte[0];
+ }
+ 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 static JsonNode read(String resource) {
+ try (var in = BbsBlsProviderConformanceTest.class.getResourceAsStream(resource)) {
+ assertNotNull(in, "Missing fixture resource: " + resource);
+ return JSON.readTree(in);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to read fixture resource: " + resource, e);
+ }
+ }
+
+ private static Stream providerSuites() {
+ List providers = List.of(
+ new ProviderFixture("pure-java", Bls12381Providers.pureJava()),
+ new ProviderFixture("wasm-zkcrypto", WasmBls12381Provider.createDefault()),
+ new ProviderFixture("blst", BlstBls12381Provider.createDefault()));
+ return providers.stream()
+ .flatMap(provider -> SUITES.stream().map(suite -> Arguments.of(provider, suite)));
+ }
+
+ private record ProviderFixture(String name, Bls12381Provider bls) {
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ private record SuiteFixture(String resourceBase, BbsCiphersuite ciphersuite) {
+ @Override
+ public String toString() {
+ return ciphersuite.name();
+ }
+ }
+}
diff --git a/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsServiceTest.java b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsServiceTest.java
new file mode 100644
index 0000000..f2d7359
--- /dev/null
+++ b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsServiceTest.java
@@ -0,0 +1,355 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import co.nstant.in.cbor.CborBuilder;
+import co.nstant.in.cbor.CborEncoder;
+import co.nstant.in.cbor.CborException;
+import co.nstant.in.cbor.model.ByteString;
+import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
+import com.bloxbean.cardano.zeroj.api.CircuitId;
+import com.bloxbean.cardano.zeroj.api.CurveId;
+import com.bloxbean.cardano.zeroj.api.ProofSystemId;
+import com.bloxbean.cardano.zeroj.api.PublicInputs;
+import com.bloxbean.cardano.zeroj.api.VerificationKeyRef;
+import com.bloxbean.cardano.zeroj.api.VerificationMaterial;
+import com.bloxbean.cardano.zeroj.api.ZkProofEnvelope;
+import com.bloxbean.cardano.zeroj.bbs.verifier.BbsZkVerifier;
+import com.bloxbean.cardano.zeroj.bbs.spi.PureJavaBbsProvider;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class BbsServiceTest {
+
+ @Test
+ void serviceSignsVerifiesAndDerivesPresentation() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("01234567890123456789012345678901"), bytes("issuer-key"));
+ List messages = List.of(bytes("name:alice"), bytes("age:42"), bytes("member:true"));
+ byte[] header = bytes("credential-v1");
+ byte[] presentationHeader = bytes("verifier-session-123");
+
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ assertTrue(service.verify(keyPair.publicKey(), signature, messages, header));
+
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, presentationHeader, new int[]{0, 2});
+
+ assertEquals(List.of(new BbsRevealedMessage(0, messages.get(0)), new BbsRevealedMessage(2, messages.get(2))),
+ presentation.revealedMessages());
+ assertTrue(service.verifyPresentation(keyPair.publicKey(), presentation));
+ }
+
+ @Test
+ void presentationCborRoundTrips() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("abcdefghijklmnopqrstuvwxyz123456"), bytes("issuer-key"));
+ List messages = List.of(bytes("status:active"), bytes("role:member"));
+ byte[] header = bytes("credential-v1");
+ byte[] presentationHeader = bytes("session");
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, presentationHeader, new int[]{1});
+
+ byte[] cbor = BbsPresentationCodec.encode(presentation);
+ BbsPresentation decoded = BbsPresentationCodec.decode(cbor);
+
+ assertEquals(presentation.proof(), decoded.proof());
+ assertArrayEquals(presentation.header(), decoded.header());
+ assertArrayEquals(presentation.presentationHeader(), decoded.presentationHeader());
+ assertEquals(presentation.revealedMessages(), decoded.revealedMessages());
+ assertArrayEquals(cbor, BbsPresentationCodec.encode(decoded));
+ assertTrue(service.verifyPresentation(keyPair.publicKey(), decoded));
+ }
+
+ @Test
+ void specOrderSignAndVerifyOverloadsWork() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("abcdefghijklmnopqrstuvwxyzabcdef"), bytes("issuer-key"));
+ List messages = List.of(bytes("status:active"), bytes("role:member"));
+ byte[] header = bytes("credential-v1");
+
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), header, messages);
+
+ assertTrue(service.verify(keyPair.publicKey(), signature, header, messages));
+ }
+
+ @Test
+ void verifyRejectsTamperedSignatureWrongPublicKeyAndModifiedMessage() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789ij"), bytes("issuer-key"));
+ BbsKeyPair wrongKeyPair = service.keyPair(bytes("012345678901234567890123456789kl"), bytes("issuer-key"));
+ List messages = List.of(bytes("given:Alice"), bytes("family:Liddell"));
+ byte[] header = bytes("credential-v1");
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ byte[] tamperedSignatureBytes = signature.bytes();
+ tamperedSignatureBytes[tamperedSignatureBytes.length - 1] ^= 0x01;
+
+ assertFalse(service.verify(
+ keyPair.publicKey(),
+ new BbsSignature(tamperedSignatureBytes, BbsCiphersuite.BLS12381_SHA256),
+ messages,
+ header));
+ assertFalse(service.verify(wrongKeyPair.publicKey(), signature, messages, header));
+ assertFalse(service.verify(
+ keyPair.publicKey(),
+ signature,
+ List.of(bytes("given:Alice"), bytes("family:Changed")),
+ header));
+ }
+
+ @Test
+ void presentationIndexValidationRejectsInvalidServiceInputs() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789mn"), bytes("issuer-key"));
+ List messages = List.of(bytes("given:Alice"), bytes("family:Liddell"));
+ byte[] header = bytes("credential-v1");
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+
+ assertThrows(BbsException.class, () -> service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, bytes("nonce"), new int[]{2}));
+ assertThrows(BbsException.class, () -> service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, bytes("nonce"), new int[]{0, 0}));
+
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, bytes("nonce"), new int[]{0});
+ BbsPresentation duplicateRevealed = new BbsPresentation(
+ presentation.proof(),
+ presentation.header(),
+ presentation.presentationHeader(),
+ List.of(new BbsRevealedMessage(0, messages.get(0)), new BbsRevealedMessage(0, messages.get(0))));
+
+ assertThrows(BbsException.class, () -> service.verifyPresentation(keyPair.publicKey(), duplicateRevealed));
+ }
+
+ @Test
+ void presentationCodecRejectsMalformedNonCanonicalAndDuplicateIndexCbor() throws Exception {
+ BbsPresentation presentation = samplePresentation();
+
+ assertThrows(BbsException.class, () -> BbsPresentationCodec.decode(new byte[]{0}));
+ assertThrows(BbsException.class, () -> BbsPresentationCodec.decode(envelope(presentation, 2, false, false)));
+ assertThrows(BbsException.class, () -> BbsPresentationCodec.decode(envelope(presentation, 1, true, false)));
+ assertThrows(BbsException.class, () -> BbsPresentationCodec.decode(envelope(presentation, 1, false, true)));
+ }
+
+ @Test
+ void presentationCodecAppliesFieldLengthCaps() {
+ BbsPresentation presentation = samplePresentation();
+ byte[] oversizedHeader = new byte[65_536];
+ BbsPresentation oversized = new BbsPresentation(
+ presentation.proof(),
+ oversizedHeader,
+ presentation.presentationHeader(),
+ presentation.revealedMessages());
+
+ assertThrows(BbsException.class, () -> BbsPresentationCodec.encode(oversized));
+ }
+
+ @Test
+ void zkVerifierAcceptsPresentationEnvelope() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789ab"), bytes("issuer-key"));
+ List messages = List.of(bytes("given:Alice"), bytes("family:Liddell"), bytes("age:20"));
+ byte[] header = bytes("credential-v1");
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, bytes("verifier-nonce"), new int[]{0});
+
+ ZkProofEnvelope envelope = ZkProofEnvelope.builder()
+ .proofSystem(ProofSystemId.BBS)
+ .curve(CurveId.BLS12_381)
+ .circuitId(new CircuitId("bbs-selective-disclosure"))
+ .proofBytes(BbsPresentationCodec.encode(presentation))
+ .publicInputs(new PublicInputs(List.of()))
+ .vkRef(new VerificationKeyRef.ById("issuer-key"))
+ .proofFormat(BbsCiphersuite.DEFAULT_PROOF_FORMAT)
+ .build();
+ VerificationMaterial material = VerificationMaterial.of(
+ keyPair.publicKey().bytes(),
+ ProofSystemId.BBS,
+ CurveId.BLS12_381,
+ new CircuitId("bbs-selective-disclosure"));
+
+ var result = new BbsZkVerifier(service).verify(envelope, material);
+
+ assertTrue(result.proofValid(), result.message().orElse(""));
+ }
+
+ @Test
+ void zkVerifierAcceptsShake256PresentationEnvelope() {
+ BbsService service = BbsService.pureJava(BbsCiphersuite.BLS12381_SHAKE256);
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789cd"), bytes("issuer-key"));
+ List messages = List.of(bytes("given:Alice"), bytes("family:Liddell"), bytes("age:20"));
+ byte[] header = bytes("credential-v1");
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, header, bytes("verifier-nonce"), new int[]{0, 2});
+
+ ZkProofEnvelope envelope = ZkProofEnvelope.builder()
+ .proofSystem(ProofSystemId.BBS)
+ .curve(CurveId.BLS12_381)
+ .circuitId(new CircuitId("bbs-selective-disclosure-shake"))
+ .proofBytes(BbsPresentationCodec.encode(presentation))
+ .publicInputs(new PublicInputs(List.of()))
+ .vkRef(new VerificationKeyRef.ById("issuer-key"))
+ .proofFormat(BbsCiphersuite.DEFAULT_PROOF_FORMAT)
+ .build();
+ VerificationMaterial material = VerificationMaterial.of(
+ keyPair.publicKey().bytes(),
+ ProofSystemId.BBS,
+ CurveId.BLS12_381,
+ new CircuitId("bbs-selective-disclosure-shake"));
+
+ var result = new BbsZkVerifier().verify(envelope, material);
+
+ assertTrue(result.proofValid(), result.message().orElse(""));
+ }
+
+ @Test
+ void signingAndProofGenerationUseSecretScalarProviderBoundary() {
+ var bls = new CountingBlsProvider(Bls12381Providers.pureJava());
+ var provider = new PureJavaBbsProvider(BbsCiphersuite.BLS12381_SHA256, bls);
+ BbsService service = new BbsService(provider, new SecureRandom(new byte[]{1, 2, 3, 4}));
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789ef"), bytes("issuer-key"));
+ List messages = List.of(bytes("given:Alice"), bytes("family:Liddell"), bytes("age:20"));
+
+ assertTrue(bls.g2SecretScalarMulCalls > 0, "SkToPk must use the secret G2 scalar boundary");
+
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, bytes("credential-v1"));
+ assertTrue(bls.g1SecretScalarMulCalls > 0, "Sign must use the secret G1 scalar boundary");
+ int afterSignSecretCalls = bls.g1SecretScalarMulCalls;
+
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(), signature, messages, bytes("credential-v1"), bytes("verifier-nonce"), new int[]{0});
+
+ assertTrue(bls.g1SecretScalarMulCalls > afterSignSecretCalls,
+ "ProofGen must use the secret G1 scalar boundary");
+ assertTrue(service.verifyPresentation(keyPair.publicKey(), presentation));
+ }
+
+ @Test
+ void serviceFactoryAcceptsExplicitBlsProvider() {
+ var bls = new CountingBlsProvider(Bls12381Providers.pureJava());
+ BbsService service = BbsService.withBlsProvider(BbsCiphersuite.BLS12381_SHAKE256, bls);
+
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789gh"), bytes("issuer-key"));
+
+ assertEquals(BbsCiphersuite.BLS12381_SHAKE256, service.provider().ciphersuite());
+ assertEquals(BbsCiphersuite.BLS12381_SHAKE256, keyPair.publicKey().ciphersuite());
+ assertTrue(bls.g2SecretScalarMulCalls > 0, "Selected BLS provider must be used for SkToPk");
+ }
+
+ private static byte[] bytes(String text) {
+ return text.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static BbsPresentation samplePresentation() {
+ BbsService service = BbsService.pureJava();
+ BbsKeyPair keyPair = service.keyPair(bytes("012345678901234567890123456789op"), bytes("issuer-key"));
+ List messages = List.of(bytes("given:Alice"), bytes("family:Liddell"), bytes("age:20"));
+ byte[] header = bytes("credential-v1");
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ return service.derivePresentation(keyPair.publicKey(), signature, messages, header, bytes("nonce"), new int[]{0, 2});
+ }
+
+ private static byte[] envelope(
+ BbsPresentation presentation,
+ int version,
+ boolean reversedKeys,
+ boolean duplicateRevealedIndex) throws CborException {
+ co.nstant.in.cbor.model.Array revealed = new co.nstant.in.cbor.model.Array();
+ for (BbsRevealedMessage message : presentation.revealedMessages()) {
+ co.nstant.in.cbor.model.Array pair = new co.nstant.in.cbor.model.Array();
+ pair.add(new UnsignedInteger(duplicateRevealedIndex ? 0 : message.index()));
+ pair.add(new ByteString(message.message()));
+ revealed.add(pair);
+ }
+
+ co.nstant.in.cbor.model.Map map = new co.nstant.in.cbor.model.Map();
+ if (reversedKeys) {
+ map.put(new UnsignedInteger(6), revealed);
+ map.put(new UnsignedInteger(5), new ByteString(presentation.presentationHeader()));
+ map.put(new UnsignedInteger(4), new ByteString(presentation.header()));
+ map.put(new UnsignedInteger(3), new ByteString(presentation.proof().bytes()));
+ map.put(new UnsignedInteger(2), new UnicodeString(presentation.proof().ciphersuite().ciphersuiteId()));
+ map.put(new UnsignedInteger(1), new UnsignedInteger(version));
+ } else {
+ map.put(new UnsignedInteger(1), new UnsignedInteger(version));
+ map.put(new UnsignedInteger(2), new UnicodeString(presentation.proof().ciphersuite().ciphersuiteId()));
+ map.put(new UnsignedInteger(3), new ByteString(presentation.proof().bytes()));
+ map.put(new UnsignedInteger(4), new ByteString(presentation.header()));
+ map.put(new UnsignedInteger(5), new ByteString(presentation.presentationHeader()));
+ map.put(new UnsignedInteger(6), revealed);
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CborEncoder encoder = new CborEncoder(baos);
+ if (reversedKeys) {
+ encoder.nonCanonical();
+ }
+ encoder.encode(new CborBuilder().add(map).build());
+ return baos.toByteArray();
+ }
+
+ private static final class CountingBlsProvider implements Bls12381Provider {
+ private final Bls12381Provider delegate;
+ private int g1SecretScalarMulCalls;
+ private int g2SecretScalarMulCalls;
+
+ private CountingBlsProvider(Bls12381Provider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String id() {
+ return "counting-" + delegate.id();
+ }
+
+ @Override
+ public G1Point g1Generator() {
+ return delegate.g1Generator();
+ }
+
+ @Override
+ public G2Point g2Generator() {
+ return delegate.g2Generator();
+ }
+
+ @Override
+ public G1Point g1ScalarMul(G1Point point, BigInteger scalar) {
+ return delegate.g1ScalarMul(point, scalar);
+ }
+
+ @Override
+ public G2Point g2ScalarMul(G2Point point, BigInteger scalar) {
+ return delegate.g2ScalarMul(point, scalar);
+ }
+
+ @Override
+ public G1Point g1SecretScalarMul(G1Point point, BigInteger scalar) {
+ g1SecretScalarMulCalls++;
+ return delegate.g1SecretScalarMul(point, scalar);
+ }
+
+ @Override
+ public G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) {
+ g2SecretScalarMulCalls++;
+ return delegate.g2SecretScalarMul(point, scalar);
+ }
+
+ @Override
+ public boolean pairingProductIsIdentity(G1Point[] g1Points, G2Point[] g2Points) {
+ return delegate.pairingProductIsIdentity(g1Points, g2Points);
+ }
+ }
+}
diff --git a/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsDraft10VectorTest.java b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsDraft10VectorTest.java
new file mode 100644
index 0000000..00cabf5
--- /dev/null
+++ b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsDraft10VectorTest.java
@@ -0,0 +1,252 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import com.bloxbean.cardano.zeroj.bbs.internal.CfrgBbsCore;
+import com.bloxbean.cardano.zeroj.bbs.spi.BbsProviders;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CfrgBbsDraft10VectorTest {
+ private static final BbsCiphersuite SUITE = BbsCiphersuite.BLS12381_SHA256;
+ private static final Properties VECTORS = loadVectors();
+
+ @Test
+ void keyGenAndSkToPk_matchDraft10Vector() {
+ var provider = BbsProviders.pureJava();
+
+ BbsSecretKey secretKey = provider.keyGen(hex("key_material"), hex("key_info"));
+ BbsPublicKey publicKey = provider.skToPk(secretKey);
+
+ assertArrayEquals(hex("sk"), secretKey.toBytes());
+ assertArrayEquals(hex("pk"), publicKey.bytes());
+ }
+
+ @Test
+ void messagesToScalars_matchDraft10Vector() {
+ List messages = messages();
+ List actual = CfrgBbsCore.messagesToScalars(messages, SUITE);
+ List expected = csv("message_scalars");
+
+ assertEquals(expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); i++) {
+ assertArrayEquals(hexString(expected.get(i)), com.bloxbean.cardano.zeroj.bbs.internal.BbsCodec.scalarToBytesAllowZero(actual.get(i)));
+ }
+ }
+
+ @Test
+ void generators_matchDraft10Vector() {
+ var generators = CfrgBbsCore.createGenerators(11, SUITE, Bls12381Providers.pureJava());
+ List expected = csv("generators");
+
+ assertArrayEquals(hexString(expected.get(0)), Bls12381Codecs.g1ToCompressed(generators.q1()));
+ for (int i = 0; i < generators.h().size(); i++) {
+ assertArrayEquals(hexString(expected.get(i + 1)), Bls12381Codecs.g1ToCompressed(generators.h().get(i)));
+ }
+ }
+
+ @Test
+ void signAndVerify_matchDraft10SingleMessageVector() {
+ var provider = BbsProviders.pureJava();
+ BbsSecretKey secretKey = new BbsSecretKey(new BigInteger(1, hex("sk")), SUITE);
+ BbsPublicKey publicKey = new BbsPublicKey(hex("pk"), SUITE);
+ List oneMessage = messages().subList(0, 1);
+
+ BbsSignature signature = provider.sign(secretKey, publicKey, oneMessage, hex("header"));
+
+ assertArrayEquals(hex("single_message_signature"), signature.bytes());
+ assertTrue(provider.verify(publicKey, signature, oneMessage, hex("header")));
+ assertFalse(provider.verify(publicKey, tamper(signature), oneMessage, hex("header")));
+ assertFalse(provider.verify(publicKey, signature, oneMessage, hexString("00")));
+ }
+
+ @Test
+ void signAndVerify_matchDraft10MultiMessageVector() {
+ var provider = BbsProviders.pureJava();
+ BbsSecretKey secretKey = new BbsSecretKey(new BigInteger(1, hex("sk")), SUITE);
+ BbsPublicKey publicKey = new BbsPublicKey(hex("pk"), SUITE);
+ List messages = messages();
+
+ BbsSignature signature = provider.sign(secretKey, publicKey, messages, hex("header"));
+
+ assertArrayEquals(hex("multi_message_signature"), signature.bytes());
+ assertTrue(provider.verify(publicKey, signature, messages, hex("header")));
+ assertFalse(provider.verify(publicKey, signature, replace(messages, 2, hexString("01")), hex("header")));
+ }
+
+ @Test
+ void seededRandomScalars_matchDraft10MockVector() {
+ List scalars = CfrgBbsCore.seededRandomScalars(hex("mock_seed"), hex("mock_dst"), 10, SUITE);
+ List expected = csv("mock_scalars");
+
+ assertEquals(expected.size(), scalars.size());
+ for (int i = 0; i < expected.size(); i++) {
+ assertArrayEquals(hexString(expected.get(i)), com.bloxbean.cardano.zeroj.bbs.internal.BbsCodec.scalarToBytesAllowZero(scalars.get(i)));
+ }
+ }
+
+ @Test
+ void proofGenAndVerify_matchDraft10SingleMessageProofVector() {
+ List randomScalars = csv("single_proof_random_scalars").stream()
+ .map(hex -> new BigInteger(1, hexString(hex)))
+ .toList();
+ byte[] proof = CfrgBbsCore.proofGenWithRandomScalars(
+ hex("pk"),
+ hex("single_message_signature"),
+ messages().subList(0, 1),
+ hex("header"),
+ hex("presentation_header"),
+ new int[]{0},
+ SUITE,
+ Bls12381Providers.pureJava(),
+ randomScalars);
+
+ assertArrayEquals(hex("single_proof"), proof);
+ assertTrue(CfrgBbsCore.proofVerify(
+ hex("pk"),
+ proof,
+ hex("header"),
+ hex("presentation_header"),
+ messages().subList(0, 1),
+ new int[]{0},
+ SUITE,
+ Bls12381Providers.pureJava()));
+ }
+
+ @Test
+ void proofGenAndVerify_matchDraft10AllDisclosedProofVector() {
+ List messages = messages();
+ int[] allIndexes = java.util.stream.IntStream.range(0, messages.size()).toArray();
+ List randomScalars = CfrgBbsCore.seededRandomScalars(hex("mock_seed"), hex("mock_dst"), 5, SUITE);
+
+ byte[] proof = CfrgBbsCore.proofGenWithRandomScalars(
+ hex("pk"),
+ hex("multi_message_signature"),
+ messages,
+ hex("header"),
+ hex("presentation_header"),
+ allIndexes,
+ SUITE,
+ Bls12381Providers.pureJava(),
+ randomScalars);
+
+ assertArrayEquals(hex("all_disclosed_proof"), proof);
+ assertTrue(CfrgBbsCore.proofVerify(
+ hex("pk"),
+ proof,
+ hex("header"),
+ hex("presentation_header"),
+ messages,
+ allIndexes,
+ SUITE,
+ Bls12381Providers.pureJava()));
+ }
+
+ @Test
+ void proofGenAndVerify_matchDraft10SomeDisclosedProofVector() {
+ List messages = messages();
+ int[] disclosedIndexes = {0, 2, 4, 6};
+ List disclosedMessages = Arrays.stream(disclosedIndexes)
+ .mapToObj(messages::get)
+ .toList();
+ List randomScalars = CfrgBbsCore.seededRandomScalars(hex("mock_seed"), hex("mock_dst"), 11, SUITE);
+
+ byte[] proof = CfrgBbsCore.proofGenWithRandomScalars(
+ hex("pk"),
+ hex("multi_message_signature"),
+ messages,
+ hex("header"),
+ hex("presentation_header"),
+ disclosedIndexes,
+ SUITE,
+ Bls12381Providers.pureJava(),
+ randomScalars);
+
+ assertArrayEquals(hex("some_disclosed_proof"), proof);
+ assertTrue(CfrgBbsCore.proofVerify(
+ hex("pk"),
+ proof,
+ hex("header"),
+ hex("presentation_header"),
+ disclosedMessages,
+ disclosedIndexes,
+ SUITE,
+ Bls12381Providers.pureJava()));
+ assertFalse(CfrgBbsCore.proofVerify(
+ hex("pk"),
+ proof,
+ hex("header"),
+ hexString("00"),
+ disclosedMessages,
+ disclosedIndexes,
+ SUITE,
+ Bls12381Providers.pureJava()));
+ }
+
+ @Test
+ void invalidDisclosedIndexesReject() {
+ var provider = BbsProviders.pureJava();
+ BbsPublicKey publicKey = new BbsPublicKey(hex("pk"), SUITE);
+ BbsSignature signature = new BbsSignature(hex("multi_message_signature"), SUITE);
+ List messages = messages();
+
+ assertThrows(IllegalArgumentException.class,
+ () -> provider.proofGen(publicKey, signature, messages, hex("header"), new byte[0], new int[]{2, 2}, new java.security.SecureRandom()));
+ assertThrows(IllegalArgumentException.class,
+ () -> provider.proofGen(publicKey, signature, messages, hex("header"), new byte[0], new int[]{3, 1}, new java.security.SecureRandom()));
+ assertFalse(provider.proofVerify(publicKey, new BbsProof(hex("some_disclosed_proof"), SUITE),
+ hex("header"), hex("presentation_header"), List.of(messages.get(0), messages.get(2)), new int[]{0, 0}));
+ }
+
+ private static BbsSignature tamper(BbsSignature signature) {
+ byte[] bytes = signature.bytes();
+ bytes[bytes.length - 1] ^= 1;
+ return new BbsSignature(bytes, signature.ciphersuite());
+ }
+
+ private static List replace(List messages, int index, byte[] replacement) {
+ java.util.ArrayList copy = new java.util.ArrayList<>(messages);
+ copy.set(index, replacement);
+ return List.copyOf(copy);
+ }
+
+ private static List messages() {
+ return csv("messages").stream().map(CfrgBbsDraft10VectorTest::hexString).toList();
+ }
+
+ private static byte[] hex(String property) {
+ return hexString(VECTORS.getProperty(property));
+ }
+
+ private static byte[] hexString(String hex) {
+ if (hex.isEmpty()) {
+ return new byte[0];
+ }
+ 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 static List csv(String property) {
+ return Arrays.asList(VECTORS.getProperty(property).split(",", -1));
+ }
+
+ private static Properties loadVectors() {
+ try (var in = CfrgBbsDraft10VectorTest.class.getResourceAsStream("/cfrg-bbs/draft10/sha256.properties")) {
+ Properties properties = new Properties();
+ properties.load(in);
+ return properties;
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to load CFRG BBS draft-10 vectors", e);
+ }
+ }
+}
diff --git a/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsOfficialJsonFixtureTest.java b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsOfficialJsonFixtureTest.java
new file mode 100644
index 0000000..6e1cc94
--- /dev/null
+++ b/zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/CfrgBbsOfficialJsonFixtureTest.java
@@ -0,0 +1,251 @@
+package com.bloxbean.cardano.zeroj.bbs;
+
+import com.bloxbean.cardano.zeroj.bbs.internal.BbsCodec;
+import com.bloxbean.cardano.zeroj.bbs.internal.CfrgBbsCore;
+import com.bloxbean.cardano.zeroj.bbs.spi.BbsProviders;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CfrgBbsOfficialJsonFixtureTest {
+ private static final List SUITES = List.of(
+ new SuiteFixture("/cfrg-bbs/draft10/official/bls12-381-sha-256", BbsCiphersuite.BLS12381_SHA256),
+ new SuiteFixture("/cfrg-bbs/draft10/official/bls12-381-shake-256", BbsCiphersuite.BLS12381_SHAKE256));
+ private static final ObjectMapper JSON = new ObjectMapper();
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("suites")
+ void officialKeyPairFixtureMatches(SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/keypair.json");
+ var provider = BbsProviders.pureJava(suite.ciphersuite());
+
+ BbsSecretKey secretKey = provider.keyGen(hex(fixture, "keyMaterial"), hex(fixture, "keyInfo"));
+ BbsPublicKey publicKey = provider.skToPk(secretKey);
+
+ assertArrayEquals(hex(fixture.at("/keyPair/secretKey")), secretKey.toBytes());
+ assertArrayEquals(hex(fixture.at("/keyPair/publicKey")), publicKey.bytes());
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("suites")
+ void officialMapMessageToScalarFixtureMatches(SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/MapMessageToScalarAsHash.json");
+ for (JsonNode c : fixture.get("cases")) {
+ BigInteger scalar = CfrgBbsCore.messagesToScalars(List.of(hex(c, "message")), suite.ciphersuite()).getFirst();
+
+ assertArrayEquals(hex(c, "scalar"), BbsCodec.scalarToBytesAllowZero(scalar));
+ }
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("suites")
+ void officialGeneratorFixtureMatches(SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/generators.json");
+ var generators = CfrgBbsCore.createGenerators(11, suite.ciphersuite(), Bls12381Providers.pureJava());
+
+ assertArrayEquals(hex(fixture, "P1"), Bls12381Codecs.g1ToCompressed(suite.ciphersuite().p1()));
+ assertArrayEquals(hex(fixture, "Q1"), Bls12381Codecs.g1ToCompressed(generators.q1()));
+ for (int i = 0; i < fixture.get("MsgGenerators").size(); i++) {
+ assertArrayEquals(hex(fixture.get("MsgGenerators").get(i)),
+ Bls12381Codecs.g1ToCompressed(generators.h().get(i)));
+ }
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("suites")
+ void officialHashToScalarFixtureMatches(SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/h2s.json");
+ BigInteger scalar = CfrgBbsCore.hashToScalar(hex(fixture, "message"), hex(fixture, "dst"), suite.ciphersuite());
+
+ assertArrayEquals(hex(fixture, "scalar"), BbsCodec.scalarToBytesAllowZero(scalar));
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("suites")
+ void officialMockedRngFixtureMatches(SuiteFixture suite) {
+ JsonNode fixture = read(suite.resourceBase() + "/mockedRng.json");
+ List scalars = CfrgBbsCore.seededRandomScalars(
+ hex(fixture, "seed"), hex(fixture, "dst"), fixture.get("count").asInt(), suite.ciphersuite());
+
+ assertEquals(fixture.get("mockedScalars").size(), scalars.size());
+ for (int i = 0; i < scalars.size(); i++) {
+ assertArrayEquals(hex(fixture.get("mockedScalars").get(i)), BbsCodec.scalarToBytesAllowZero(scalars.get(i)));
+ }
+ }
+
+ @ParameterizedTest(name = "{0} {1}")
+ @MethodSource("signatureFixtures")
+ void officialSignatureFixturesVerifyAndValidCasesSign(SuiteFixture suite, String resource) {
+ JsonNode fixture = read(resource);
+ var provider = BbsProviders.pureJava(suite.ciphersuite());
+ BbsSecretKey secretKey = new BbsSecretKey(new BigInteger(1, hex(fixture.at("/signerKeyPair/secretKey"))), suite.ciphersuite());
+ BbsPublicKey publicKey = new BbsPublicKey(hex(fixture.at("/signerKeyPair/publicKey")), suite.ciphersuite());
+ BbsSignature signature = new BbsSignature(hex(fixture, "signature"), suite.ciphersuite());
+ List messages = byteArrayList(fixture.get("messages"));
+ byte[] header = hex(fixture, "header");
+ boolean expected = fixture.at("/result/valid").asBoolean();
+
+ assertEquals(expected, provider.verify(publicKey, signature, messages, header), fixture.get("caseName").asText());
+
+ if (expected) {
+ BbsSignature generated = provider.sign(secretKey, publicKey, messages, header);
+ assertArrayEquals(signature.bytes(), generated.bytes());
+ assertSignatureTrace(fixture, publicKey.bytes(), messages, header, suite.ciphersuite());
+ }
+ }
+
+ @ParameterizedTest(name = "{0} {1}")
+ @MethodSource("proofFixtures")
+ void officialProofFixturesVerifyAndValidCasesGenerate(SuiteFixture suite, String resource) {
+ JsonNode fixture = read(resource);
+ byte[] publicKey = hex(fixture, "signerPublicKey");
+ byte[] proof = hex(fixture, "proof");
+ byte[] header = hex(fixture, "header");
+ byte[] presentationHeader = hex(fixture, "presentationHeader");
+ List messages = byteArrayList(fixture.get("messages"));
+ int[] disclosedIndexes = intArray(fixture.get("disclosedIndexes"));
+ List disclosedMessages = select(messages, disclosedIndexes);
+ boolean expected = fixture.at("/result/valid").asBoolean();
+
+ assertEquals(expected, CfrgBbsCore.proofVerify(
+ publicKey,
+ proof,
+ header,
+ presentationHeader,
+ disclosedMessages,
+ disclosedIndexes,
+ suite.ciphersuite(),
+ Bls12381Providers.pureJava()), fixture.get("caseName").asText());
+
+ if (expected) {
+ byte[] generated = CfrgBbsCore.proofGenWithRandomScalars(
+ publicKey,
+ hex(fixture, "signature"),
+ messages,
+ header,
+ presentationHeader,
+ disclosedIndexes,
+ suite.ciphersuite(),
+ Bls12381Providers.pureJava(),
+ proofRandomScalars(fixture.at("/trace/random_scalars")));
+ assertArrayEquals(proof, generated);
+ }
+ }
+
+ private static void assertSignatureTrace(
+ JsonNode fixture,
+ byte[] publicKey,
+ List messages,
+ byte[] header,
+ BbsCiphersuite ciphersuite) {
+ List scalars = CfrgBbsCore.messagesToScalars(messages, ciphersuite);
+ var generators = CfrgBbsCore.createGenerators(scalars.size() + 1, ciphersuite, Bls12381Providers.pureJava());
+ BigInteger domain = CfrgBbsCore.calculateDomain(publicKey, generators, header, ciphersuite);
+
+ assertArrayEquals(hex(fixture.at("/trace/domain")), BbsCodec.scalarToBytesAllowZero(domain));
+ assertArrayEquals(hex(fixture.at("/trace/B")),
+ Bls12381Codecs.g1ToCompressed(CfrgBbsCore.calculateB(generators, domain, scalars, ciphersuite)));
+ }
+
+ private static List proofRandomScalars(JsonNode randomScalars) {
+ List out = new ArrayList<>();
+ out.add(scalar(randomScalars, "r1"));
+ out.add(scalar(randomScalars, "r2"));
+ out.add(scalar(randomScalars, "e_tilde"));
+ out.add(scalar(randomScalars, "r1_tilde"));
+ out.add(scalar(randomScalars, "r3_tilde"));
+ for (JsonNode mTilde : randomScalars.get("m_tilde_scalars")) {
+ out.add(new BigInteger(1, hex(mTilde)));
+ }
+ return List.copyOf(out);
+ }
+
+ private static BigInteger scalar(JsonNode node, String fieldName) {
+ return new BigInteger(1, hex(node, fieldName));
+ }
+
+ private static List select(List values, int[] indexes) {
+ List out = new ArrayList<>(indexes.length);
+ for (int index : indexes) {
+ out.add(values.get(index));
+ }
+ return List.copyOf(out);
+ }
+
+ private static List byteArrayList(JsonNode array) {
+ List out = new ArrayList<>();
+ for (JsonNode item : array) {
+ out.add(hex(item));
+ }
+ return List.copyOf(out);
+ }
+
+ private static int[] intArray(JsonNode array) {
+ int[] out = new int[array.size()];
+ for (int i = 0; i < array.size(); i++) {
+ out[i] = array.get(i).asInt();
+ }
+ return out;
+ }
+
+ private static byte[] hex(JsonNode node, String fieldName) {
+ return hex(node.get(fieldName));
+ }
+
+ private static byte[] hex(JsonNode node) {
+ String hex = node.asText();
+ if (hex.isEmpty()) {
+ return new byte[0];
+ }
+ 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 static JsonNode read(String resource) {
+ try (var in = CfrgBbsOfficialJsonFixtureTest.class.getResourceAsStream(resource)) {
+ assertNotNull(in, "Missing fixture resource: " + resource);
+ return JSON.readTree(in);
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to read fixture resource: " + resource, e);
+ }
+ }
+
+ private static Stream suites() {
+ return SUITES.stream();
+ }
+
+ private static Stream signatureFixtures() {
+ return SUITES.stream()
+ .flatMap(suite -> IntStream.rangeClosed(1, 10)
+ .mapToObj(i -> Arguments.of(suite, suite.resourceBase() + "/signature/signature%03d.json".formatted(i))));
+ }
+
+ private static Stream proofFixtures() {
+ return SUITES.stream()
+ .flatMap(suite -> IntStream.rangeClosed(1, 15)
+ .mapToObj(i -> Arguments.of(suite, suite.resourceBase() + "/proof/proof%03d.json".formatted(i))));
+ }
+
+ private record SuiteFixture(String resourceBase, BbsCiphersuite ciphersuite) {
+ @Override
+ public String toString() {
+ return ciphersuite.name();
+ }
+ }
+}
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/MapMessageToScalarAsHash.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/MapMessageToScalarAsHash.json
new file mode 100644
index 0000000..cfa7413
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/MapMessageToScalarAsHash.json
@@ -0,0 +1,46 @@
+{
+ "caseName": "MapMessageToScalar fixture",
+ "dst": "4242535f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4832475f484d32535f4d41505f4d53475f544f5f5343414c41525f41535f484153485f",
+ "cases": [
+ {
+ "message": "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "scalar": "1cb5bb86114b34dc438a911617655a1db595abafac92f47c5001799cf624b430"
+ },
+ {
+ "message": "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "scalar": "154249d503c093ac2df516d4bb88b510d54fd97e8d7121aede420a25d9521952"
+ },
+ {
+ "message": "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "scalar": "0c7c4c85cdab32e6fdb0de267b16fa3212733d4e3a3f0d0f751657578b26fe22"
+ },
+ {
+ "message": "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "scalar": "4a196deafee5c23f630156ae13be3e46e53b7e39094d22877b8cba7f14640888"
+ },
+ {
+ "message": "496694774c5604ab1b2544eababcf0f53278ff50",
+ "scalar": "34c5ea4f2ba49117015a02c711bb173c11b06b3f1571b88a2952b93d0ed4cf7e"
+ },
+ {
+ "message": "515ae153e22aae04ad16f759e07237b4",
+ "scalar": "4045b39b83055cd57a4d0203e1660800fabe434004dbdc8730c21ce3f0048b08"
+ },
+ {
+ "message": "d183ddc6e2665aa4e2f088af",
+ "scalar": "064621da4377b6b1d05ecc37cf3b9dfc94b9498d7013dc5c4a82bf3bb1750743"
+ },
+ {
+ "message": "ac55fb33a75909ed",
+ "scalar": "34ac9196ace0a37e147e32319ea9b3d8cc7d21870d3c3ba071246859cca49b02"
+ },
+ {
+ "message": "96012096",
+ "scalar": "57eb93f417c43200e9784fa5ea5a59168d3dbc38df707a13bb597c871b2a5f74"
+ },
+ {
+ "message": "",
+ "scalar": "08e3afeb2b4f2b5f907924ef42856616e6f2d5f1fb373736db1cca32707a7d16"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/generators.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/generators.json
new file mode 100644
index 0000000..967b6f2
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/generators.json
@@ -0,0 +1,16 @@
+{
+ "P1": "a8ce256102840821a3e94ea9025e4662b205762f9776b3a766c872b948f1fd225e7c59698588e70d11406d161b4e28c9",
+ "Q1": "a9ec65b70a7fbe40c874c9eb041c2cb0a7af36ccec1bea48fa2ba4c2eb67ef7f9ecb17ed27d38d27cdeddff44c8137be",
+ "MsgGenerators": [
+ "98cd5313283aaf5db1b3ba8611fe6070d19e605de4078c38df36019fbaad0bd28dd090fd24ed27f7f4d22d5ff5dea7d4",
+ "a31fbe20c5c135bcaa8d9fc4e4ac665cc6db0226f35e737507e803044093f37697a9d452490a970eea6f9ad6c3dcaa3a",
+ "b479263445f4d2108965a9086f9d1fdc8cde77d14a91c856769521ad3344754cc5ce90d9bc4c696dffbc9ef1d6ad1b62",
+ "ac0401766d2128d4791d922557c7b4d1ae9a9b508ce266575244a8d6f32110d7b0b7557b77604869633bb49afbe20035",
+ "b95d2898370ebc542857746a316ce32fa5151c31f9b57915e308ee9d1de7db69127d919e984ea0747f5223821b596335",
+ "8f19359ae6ee508157492c06765b7df09e2e5ad591115742f2de9c08572bb2845cbf03fd7e23b7f031ed9c7564e52f39",
+ "abc914abe2926324b2c848e8a411a2b6df18cbe7758db8644145fefb0bf0a2d558a8c9946bd35e00c69d167aadf304c1",
+ "80755b3eb0dd4249cbefd20f177cee88e0761c066b71794825c9997b551f24051c352567ba6c01e57ac75dff763eaa17",
+ "82701eb98070728e1769525e73abff1783cedc364adb20c05c897a62f2ab2927f86f118dcb7819a7b218d8f3fee4bd7f",
+ "a1f229540474f4d6f1134761b92b788128c7ac8dc9b0c52d59493132679673032ac7db3fb3d79b46b13c1c41ee495bca"
+ ]
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/h2s.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/h2s.json
new file mode 100644
index 0000000..c116f76
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/h2s.json
@@ -0,0 +1,6 @@
+{
+ "caseName": "Hash to scalar output",
+ "message": "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "dst": "4242535f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4832475f484d32535f4832535f",
+ "scalar": "0f90cbee27beb214e6545becb8404640d3612da5d6758dffeccd77ed7169807c"
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/keypair.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/keypair.json
new file mode 100644
index 0000000..4b203c7
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/keypair.json
@@ -0,0 +1,10 @@
+{
+ "caseName": "key pair fixture",
+ "keyMaterial": "746869732d49532d6a7573742d616e2d546573742d494b4d2d746f2d67656e65726174652d246528724074232d6b6579",
+ "keyInfo": "746869732d49532d736f6d652d6b65792d6d657461646174612d746f2d62652d757365642d696e2d746573742d6b65792d67656e",
+ "keyDst": "4242535f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4832475f484d32535f4b455947454e5f4453545f",
+ "keyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/mockedRng.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/mockedRng.json
new file mode 100644
index 0000000..b12218c
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/mockedRng.json
@@ -0,0 +1,18 @@
+{
+ "caseName": "mocked random scalars",
+ "seed": "332e313431353932363533353839373933323338343632363433333833323739",
+ "dst": "4242535f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4832475f484d32535f4d4f434b5f52414e444f4d5f5343414c4152535f4453545f",
+ "count": 10,
+ "mockedScalars": [
+ "04f8e2518993c4383957ad14eb13a023c4ad0c67d01ec86eeb902e732ed6df3f",
+ "5d87c1ba64c320ad601d227a1b74188a41a100325cecf00223729863966392b1",
+ "0444607600ac70482e9c983b4b063214080b9e808300aa4cc02a91b3a92858fe",
+ "548cd11eae4318e88cda10b4cd31ae29d41c3a0b057196ee9cf3a69d471e4e94",
+ "2264b06a08638b69b4627756a62f08e0dc4d8240c1b974c9c7db779a769892f4",
+ "4d99352986a9f8978b93485d21525244b21b396cf61f1d71f7c48e3fbc970a42",
+ "5ed8be91662386243a6771fbdd2c627de31a44220e8d6f745bad5d99821a4880",
+ "62ff1734b939ddd87beeb37a7bbcafa0a274cbc1b07384198f0e88398272208d",
+ "05c2a0af016df58e844db8944082dcaf434de1b1e2e7136ec8a99b939b716223",
+ "485e2adab17b76f5334c95bf36c03ccf91cef77dcfcdc6b8a69e2090b3156663"
+ ]
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof001.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof001.json
new file mode 100644
index 0000000..a5816bb
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof001.json
@@ -0,0 +1,34 @@
+{
+ "caseName": "valid single message signature, single-message revealed proof",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "84773160b824e194073a57493dac1a20b667af70cd2352d8af241c77658da5253aa8458317cca0eae615690d55b1f27164657dcafee1d5c1973947aa70e2cfbb4c892340be5969920d0916067b4565a0",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"
+ ],
+ "disclosedIndexes": [
+ 0
+ ],
+ "proof": "94916292a7a6bade28456c601d3af33fcf39278d6594b467e128a3f83686a104ef2b2fcf72df0215eeaf69262ffe8194a19fab31a82ddbe06908985abc4c9825788b8a1610942d12b7f5debbea8985296361206dbace7af0cc834c80f33e0aadaeea5597befbb651827b5eed5a66f1a959bb46cfd5ca1a817a14475960f69b32c54db7587b5ee3ab665fbd37b506830a49f21d592f5e634f47cee05a025a2f8f94e73a6c15f02301d1178a92873b6e8634bafe4983c3e15a663d64080678dbf29417519b78af042be2b3e1c4d08b8d520ffab008cbaaca5671a15b22c239b38e940cfeaa5e72104576a9ec4a6fad78c532381aeaa6fb56409cef56ee5c140d455feeb04426193c57086c9b6d397d9418",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "60ca409f6b0563f687fc471c63d2819f446f39c23bb540925d9d4254ac58f337",
+ "r2": "2ceff4982de0c913090f75f081df5ec594c310bb48c17cfdaab5332a682ef811",
+ "e_tilde": "6101c4404895f3dff87ab39c34cb995af07e7139e6b3847180ffdd1bc8c313cd",
+ "r1_tilde": "0dfcffd97a6ecdebef3c9c114b99d7a030c998d938905f357df62822dee072e8",
+ "r3_tilde": "639e3417007d38e5d34ba8c511e836768ddc2669fdd3faff5c14ad27ac2b2da1",
+ "m_tilde_scalars": []
+ },
+ "A_bar": "94916292a7a6bade28456c601d3af33fcf39278d6594b467e128a3f83686a104ef2b2fcf72df0215eeaf69262ffe8194",
+ "B_bar": "a19fab31a82ddbe06908985abc4c9825788b8a1610942d12b7f5debbea8985296361206dbace7af0cc834c80f33e0aad",
+ "D": "aeea5597befbb651827b5eed5a66f1a959bb46cfd5ca1a817a14475960f69b32c54db7587b5ee3ab665fbd37b506830a",
+ "T1": "a862fa5d3ab4c264c22b8a02636fd4030e8b14ac20dee14e08fdb6cfc445432c08abb49ec111c1eb9d90abef50134a60",
+ "T2": "ab9543a6b04303e997621d3d5cbd85924e7e69da498a2a9e9d3a8b01f39259c9c5920bd530de1d3b0afb99eb0c549d5a",
+ "domain": "25d57fab92a8274c68fde5c3f16d4b275e4a156f211ae34b3ab32fbaf506ed5c",
+ "challenge": "32381aeaa6fb56409cef56ee5c140d455feeb04426193c57086c9b6d397d9418"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof002.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof002.json
new file mode 100644
index 0000000..d0b868b
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof002.json
@@ -0,0 +1,52 @@
+{
+ "caseName": "valid multi-message signature, all messages revealed proof",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "proof": "b1f468aec2001c4f54cb56f707c6222a43e5803a25b2253e67b2210ab2ef9eab52db2d4b379935c4823281eaf767fd37b08ce80dc65de8f9769d27099ae649ad4c9b4bd2cc23edcba52073a298087d2495e6d57aaae051ef741adf1cbce65c64a73c8c97264177a76c4a03341956d2ae45ed3438ce598d5cda4f1bf9507fecef47855480b7b30b5e4052c92a4360110c67327365763f5aa9fb85ddcbc2975449b8c03db1216ca66b310f07d0ccf12ab460cdc6003b677fed36d0a23d0818a9d4d098d44f749e91008cf50e8567ef936704c8277b7710f41ab7e6e16408ab520edc290f9801349aee7b7b4e318e6a76e028e1dea911e2e7baec6a6a174da1a22362717fbae1cd961d7bf4adce1d31c2ab",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "60ca409f6b0563f687fc471c63d2819f446f39c23bb540925d9d4254ac58f337",
+ "r2": "2ceff4982de0c913090f75f081df5ec594c310bb48c17cfdaab5332a682ef811",
+ "e_tilde": "6101c4404895f3dff87ab39c34cb995af07e7139e6b3847180ffdd1bc8c313cd",
+ "r1_tilde": "0dfcffd97a6ecdebef3c9c114b99d7a030c998d938905f357df62822dee072e8",
+ "r3_tilde": "639e3417007d38e5d34ba8c511e836768ddc2669fdd3faff5c14ad27ac2b2da1",
+ "m_tilde_scalars": []
+ },
+ "A_bar": "b1f468aec2001c4f54cb56f707c6222a43e5803a25b2253e67b2210ab2ef9eab52db2d4b379935c4823281eaf767fd37",
+ "B_bar": "b08ce80dc65de8f9769d27099ae649ad4c9b4bd2cc23edcba52073a298087d2495e6d57aaae051ef741adf1cbce65c64",
+ "D": "a73c8c97264177a76c4a03341956d2ae45ed3438ce598d5cda4f1bf9507fecef47855480b7b30b5e4052c92a4360110c",
+ "T1": "9881efa96b2411626d490e399eb1c06badf23c2c0760bd403f50f45a6b470c5a9dbeef53a27916f2f165085a3878f1f4",
+ "T2": "b9f8cf9271d10a04ae7116ad021f4b69c435d20a5af10ddd8f5b1ec6b9b8b91605aca76a140241784b7f161e21dfc3e7",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "28e1dea911e2e7baec6a6a174da1a22362717fbae1cd961d7bf4adce1d31c2ab"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof003.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof003.json
new file mode 100644
index 0000000..9e66110
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof003.json
@@ -0,0 +1,53 @@
+{
+ "caseName": "valid multi-message signature, multiple messages revealed proof",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof004.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof004.json
new file mode 100644
index 0000000..5e8514b
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof004.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (different presentation header)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "011594ba7f95b3b470ea4102dd5899de3a042e5104d3ea01d15e6780d831d2be",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "different presentation header"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof005.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof005.json
new file mode 100644
index 0000000..ec3f77d
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof005.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (wrong public key)",
+ "signerPublicKey": "b064bd8d1ba99503cbb7f9d7ea00bce877206a85b1750e5583dd9399828a4d20610cb937ea928d90404c239b2835ffb104220a9c66a4c9ed3b54c0cac9ea465d0429556b438ceefb59650ddf67e7a8f103677561b7ef7fe3c3357ec6b94d41c6",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "wrong public key"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof006.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof006.json
new file mode 100644
index 0000000..7d523a1
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof006.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (modified messages)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "7385ee1a722e00e173b4cdb1c1e0c3fb379403a31b337d3778c447d9da664ac876b0f7c5587d9e994c51f9e2b6de09c0f1d0f3b39b275a96da4926c22e55166998b8c4e90372820c007ceb27bd34ec4ebfab63fea4dcc88d95f58b25ffd35b041f3fe994",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "modified messages"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof007.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof007.json
new file mode 100644
index 0000000..943dae1
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof007.json
@@ -0,0 +1,56 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (extra message un-revealed in proof)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ "",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 9
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "extra message un-revealed in proof"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof008.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof008.json
new file mode 100644
index 0000000..242d3c1
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof008.json
@@ -0,0 +1,56 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (extra message invalid message un-revealed in proof)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ "",
+ "96012096"
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 9
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "extra message invalid message un-revealed in proof"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof009.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof009.json
new file mode 100644
index 0000000..9491590
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof009.json
@@ -0,0 +1,52 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (missing message revealed in proof)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "missing message revealed in proof"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof010.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof010.json
new file mode 100644
index 0000000..3574f13
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof010.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (re-ordered messages)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 4,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "re-ordered messages"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof011.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof011.json
new file mode 100644
index 0000000..786efb2
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof011.json
@@ -0,0 +1,56 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (extra valid message, modified total message count)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ "",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 9
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "extra valid message, modified total message count"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof012.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof012.json
new file mode 100644
index 0000000..6d32527
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof012.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (truncated proof, one less undisclosed message)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870",
+ "result": {
+ "valid": false,
+ "reason": "truncated proof, one less undisclosed message"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof013.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof013.json
new file mode 100644
index 0000000..53b0ba2
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof013.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (different header)",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "ffeeddccbbaa00998877665544332211",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a",
+ "result": {
+ "valid": false,
+ "reason": "different header"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof014.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof014.json
new file mode 100644
index 0000000..a23c728
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof014.json
@@ -0,0 +1,53 @@
+{
+ "caseName": "valid multi-message signature, multiple messages revealed proof, no header",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8c87e2080859a97299c148427cd2fcf390d24bea850103a9748879039262ecf4f42206f6ef767f298b6a96b424c1e86c26f8fba62212d0e05b95261c2cc0e5fdc63a32731347e810fd12e9c58355aa0d",
+ "header": "",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "81925c2e525d9fbb0ba95b438b5a13fff5874c7c0515c193628d7d143ddc3bb487771ad73658895997a88dd5b254ed29abc019bfca62c09b8dafb37e5f09b1d380e084ec3623d071ec38d6b8602af93aa0ddbada307c9309cca86be16db53dc7ac310574f509c712bb1a181d64ea3c1ee075c018a2bc773e2480b5c033ccb9bfea5af347a88ab83746c9342ba76db3675ff70ce9006d166fd813a81b448a632216521c864594f3f92965974914992f8d1845230915b11680cf44b25886c5670904ac2d88255c8c31aea7b072e9c4eb7e4c3fdd38836ae9d2e9fa271c8d9fd42f669a9938aeeba9d8ae613bf11f489ce947616f5cbaee95511dfaa5c73d85e4ddd2f29340f821dc2fb40db3eae5f5bc08467eb195e38d7d436b63e556ea653168282a23b53d5792a107f85b1203f82aab46f6940650760e5b320261ffc0ca5f15917b51e7d2ad4bcbec94de792e229db663abff23af392a5e73ce115c27e8492ec24a0815091c69874dbd9dae2d2eed000810c748a798a78a804a39034c6e745cee455812cc982eea7105948b2cb55b82278a77237fcbec4748e2d2255af0994dd09dba8ac60515a39b24632a2c1c840c4a70506add5b2eb0be9ff66e3ea8deae666f198edfbb1391c6834e6df4f1026d",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "81925c2e525d9fbb0ba95b438b5a13fff5874c7c0515c193628d7d143ddc3bb487771ad73658895997a88dd5b254ed29",
+ "B_bar": "abc019bfca62c09b8dafb37e5f09b1d380e084ec3623d071ec38d6b8602af93aa0ddbada307c9309cca86be16db53dc7",
+ "D": "ac310574f509c712bb1a181d64ea3c1ee075c018a2bc773e2480b5c033ccb9bfea5af347a88ab83746c9342ba76db367",
+ "T1": "ada552bd7ee0d6914b89eaa0e9426b3bdbdfa7ecac26b3c118aefefc577095e894c1b4a828c184e091a563e09763f3a9",
+ "T2": "818dd907bf0321cf982648f91d7201b357358d3b2f6f7678afa722d89bbe5eba4415e4a65567a03292d9c7859da20cad",
+ "domain": "41c5fe0290d0da734ce9bba57bfe0dfc14f3f9cfef18a0d7438cf2075fd71cc7",
+ "challenge": "4a70506add5b2eb0be9ff66e3ea8deae666f198edfbb1391c6834e6df4f1026d"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof015.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof015.json
new file mode 100644
index 0000000..f61fbe6
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/proof/proof015.json
@@ -0,0 +1,53 @@
+{
+ "caseName": "valid multi-message signature, multiple messages revealed proof, no presentation header",
+ "signerPublicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c",
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135672556358e78b5398f1a547a2a98dfe16230f244ba742dea737e4f810b4d94e03ac068ef840aaadf12b2ed51d3fb774c2a0a620019fd1f39c52c6f89a0e6067e3039413a91129791b2af215a82ad2356b6bc305c1d7a828fe519619dd026eaaf07ea81cee52b21aab3e8320519bf37c2bb228a8b580f899d84327bdc5e84a66000e8bac17d2fa039bb2246c8eacc623ccd9eb26e184a96a9e3a6702e1dbafe194772394b05251f72bcd2d20f542b15b2406f899791f6f285c7b469e7c7b9624147f305c38c903273a949f6e85b9774aeeccfafa432e2cdd7c8f97d1687741ed30d725444428dd87d9884711d9a46baaf0c04b03a2a228b7033be0841880134b03b15f698756eca5f37503a0411a9586d3027a8b8b9118e95a9949b2719e85e4a669d9e4b7bb6d4544c8cc558c30d79f9c85a87e1a95611400b7c7dac5673d800",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "44679831fe60eca50938ef0e812e2a9284ad7971b6932a38c7303538b712e457",
+ "r2": "6481692f89086cce11779e847ff884db8eebb85a13e81b2d0c79d6c1062069d8",
+ "e_tilde": "721ce4c4c148a1d5826f326af6fd6ac2844f29533ba4127c3a43d222d51b7081",
+ "r1_tilde": "1ecfaf5a079b0504b00a1f0d6fe8857291dd798291d7ad7454b398114393f37f",
+ "r3_tilde": "0a4b3d59b34707bb9999bc6e2a6d382a2d2e214bff36ecd88639a14124b1622e",
+ "m_tilde_scalars": [
+ "7217411a9e329c7a5705e8db552274646e2949d62c288d7537dd62bc284715e4",
+ "67d4d43660746759f598caac106a2b5f58ccd1c3eefaec31841a4f77d2548870",
+ "715d965b1c3912d20505b381470ff1a528700b673e50ba89fd287e13171cc137",
+ "4d3281a149674e58c9040fc7a10dd92cb9c7f76f6f0815a1afc3b09d74b92fe4",
+ "438feebaa5894ca0da49992df2c97d872bf153eab07e08ff73b28131c46ff415",
+ "602b723c8bbaec1b057d70f18269ae5e6de6197a5884967b03b933fa80006121"
+ ]
+ },
+ "A_bar": "a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc4151",
+ "B_bar": "99462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f",
+ "D": "897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac48135",
+ "T1": "84719c2b5bb275ee74913dbf95fb9054f690c8e4035f1259e184e9024544bc4bbea9c244e7897f9db7c82b7b14b27d28",
+ "T2": "8f5f191c956aefd5c960e57d2dfbab6761eb0ebc5efdba1aca1403dcc19e05296b16c9feb7636cb4ef2a360c5a148483",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47",
+ "challenge": "669d9e4b7bb6d4544c8cc558c30d79f9c85a87e1a95611400b7c7dac5673d800"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature001.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature001.json
new file mode 100644
index 0000000..5c2254d
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature001.json
@@ -0,0 +1,19 @@
+{
+ "caseName": "valid single message signature",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"
+ ],
+ "signature": "84773160b824e194073a57493dac1a20b667af70cd2352d8af241c77658da5253aa8458317cca0eae615690d55b1f27164657dcafee1d5c1973947aa70e2cfbb4c892340be5969920d0916067b4565a0",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "B": "92d264aed02bf23de022ebe778c4f929fddf829f504e451d011ed89a313b8167ac947332e1648157ceffc6e6e41ab255",
+ "domain": "25d57fab92a8274c68fde5c3f16d4b275e4a156f211ae34b3ab32fbaf506ed5c"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature002.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature002.json
new file mode 100644
index 0000000..ca9ce3b
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature002.json
@@ -0,0 +1,20 @@
+{
+ "caseName": "invalid single message signature (modified message)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ ""
+ ],
+ "signature": "84773160b824e194073a57493dac1a20b667af70cd2352d8af241c77658da5253aa8458317cca0eae615690d55b1f27164657dcafee1d5c1973947aa70e2cfbb4c892340be5969920d0916067b4565a0",
+ "result": {
+ "valid": false,
+ "reason": "modified message"
+ },
+ "trace": {
+ "B": "92d264aed02bf23de022ebe778c4f929fddf829f504e451d011ed89a313b8167ac947332e1648157ceffc6e6e41ab255",
+ "domain": "25d57fab92a8274c68fde5c3f16d4b275e4a156f211ae34b3ab32fbaf506ed5c"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature003.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature003.json
new file mode 100644
index 0000000..4642e8c
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature003.json
@@ -0,0 +1,21 @@
+{
+ "caseName": "invalid single message signature (extra unsigned message)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"
+ ],
+ "signature": "84773160b824e194073a57493dac1a20b667af70cd2352d8af241c77658da5253aa8458317cca0eae615690d55b1f27164657dcafee1d5c1973947aa70e2cfbb4c892340be5969920d0916067b4565a0",
+ "result": {
+ "valid": false,
+ "reason": "extra unsigned message"
+ },
+ "trace": {
+ "B": "92d264aed02bf23de022ebe778c4f929fddf829f504e451d011ed89a313b8167ac947332e1648157ceffc6e6e41ab255",
+ "domain": "25d57fab92a8274c68fde5c3f16d4b275e4a156f211ae34b3ab32fbaf506ed5c"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature004.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature004.json
new file mode 100644
index 0000000..d603b00
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature004.json
@@ -0,0 +1,28 @@
+{
+ "caseName": "valid multi-message signature",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "B": "84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature005.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature005.json
new file mode 100644
index 0000000..17adc78
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature005.json
@@ -0,0 +1,21 @@
+{
+ "caseName": "invalid multi-message signature (missing messages)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"
+ ],
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "result": {
+ "valid": false,
+ "reason": "missing messages"
+ },
+ "trace": {
+ "B": "84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature006.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature006.json
new file mode 100644
index 0000000..236b35a
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature006.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (re-ordered messages)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "",
+ "96012096",
+ "ac55fb33a75909ed",
+ "d183ddc6e2665aa4e2f088af",
+ "515ae153e22aae04ad16f759e07237b4",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"
+ ],
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "result": {
+ "valid": false,
+ "reason": "re-ordered messages"
+ },
+ "trace": {
+ "B": "84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature007.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature007.json
new file mode 100644
index 0000000..abeee40
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature007.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (wrong public key)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "b064bd8d1ba99503cbb7f9d7ea00bce877206a85b1750e5583dd9399828a4d20610cb937ea928d90404c239b2835ffb104220a9c66a4c9ed3b54c0cac9ea465d0429556b438ceefb59650ddf67e7a8f103677561b7ef7fe3c3357ec6b94d41c6"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "result": {
+ "valid": false,
+ "reason": "wrong public key"
+ },
+ "trace": {
+ "B": "84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature008.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature008.json
new file mode 100644
index 0000000..a8e7c08
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature008.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (different header)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "ffeeddccbbaa00998877665544332211",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "result": {
+ "valid": false,
+ "reason": "different header"
+ },
+ "trace": {
+ "B": "84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature009.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature009.json
new file mode 100644
index 0000000..e9778b2
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature009.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (re-ordered(randomly shuffled) messages)",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "ac55fb33a75909ed",
+ "",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "d183ddc6e2665aa4e2f088af",
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "96012096",
+ "515ae153e22aae04ad16f759e07237b4",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50"
+ ],
+ "signature": "8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8",
+ "result": {
+ "valid": false,
+ "reason": "re-ordered(randomly shuffled) messages"
+ },
+ "trace": {
+ "B": "84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522",
+ "domain": "6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature010.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature010.json
new file mode 100644
index 0000000..49b5124
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-sha-256/signature/signature010.json
@@ -0,0 +1,28 @@
+{
+ "caseName": "valid multi-message signature, no header",
+ "signerKeyPair": {
+ "secretKey": "60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc",
+ "publicKey": "a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c"
+ },
+ "header": "",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "8c87e2080859a97299c148427cd2fcf390d24bea850103a9748879039262ecf4f42206f6ef767f298b6a96b424c1e86c26f8fba62212d0e05b95261c2cc0e5fdc63a32731347e810fd12e9c58355aa0d",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "B": "98e38eadb6a2232cf91f41861089cda14d7e3ddef0c6eaba4d11a2732f66408f394d58301ffcc8fcfb3c89bb75136f61",
+ "domain": "41c5fe0290d0da734ce9bba57bfe0dfc14f3f9cfef18a0d7438cf2075fd71cc7"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/MapMessageToScalarAsHash.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/MapMessageToScalarAsHash.json
new file mode 100644
index 0000000..da81b8c
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/MapMessageToScalarAsHash.json
@@ -0,0 +1,46 @@
+{
+ "caseName": "MapMessageToScalar fixture",
+ "dst": "4242535f424c53313233383147315f584f463a5348414b452d3235365f535357555f524f5f4832475f484d32535f4d41505f4d53475f544f5f5343414c41525f41535f484153485f",
+ "cases": [
+ {
+ "message": "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "scalar": "1e0dea6c9ea8543731d331a0ab5f64954c188542b33c5bbc8ae5b3a830f2d99f"
+ },
+ {
+ "message": "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "scalar": "3918a40fb277b4c796805d1371931e08a314a8bf8200a92463c06054d2c56a9f"
+ },
+ {
+ "message": "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "scalar": "6642b981edf862adf34214d933c5d042bfa8f7ef343165c325131e2ffa32fa94"
+ },
+ {
+ "message": "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "scalar": "33c021236956a2006f547e22ff8790c9d2d40c11770c18cce6037786c6f23512"
+ },
+ {
+ "message": "496694774c5604ab1b2544eababcf0f53278ff50",
+ "scalar": "52b249313abbe323e7d84230550f448d99edfb6529dec8c4e783dbd6dd2a8471"
+ },
+ {
+ "message": "515ae153e22aae04ad16f759e07237b4",
+ "scalar": "2a50bdcbe7299e47e1046100aadffe35b4247bf3f059d525f921537484dd54fc"
+ },
+ {
+ "message": "d183ddc6e2665aa4e2f088af",
+ "scalar": "0e92550915e275f8cfd6da5e08e334d8ef46797ee28fa29de40a1ebccd9d95d3"
+ },
+ {
+ "message": "ac55fb33a75909ed",
+ "scalar": "4c28f612e6c6f82f51f95e1e4faaf597547f93f6689827a6dcda3cb94971d356"
+ },
+ {
+ "message": "96012096",
+ "scalar": "1db51bedc825b85efe1dab3e3ab0274fa82bbd39732be3459525faf70f197650"
+ },
+ {
+ "message": "",
+ "scalar": "27878da72f7775e709bb693d81b819dc4e9fa60711f4ea927740e40073489e78"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/generators.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/generators.json
new file mode 100644
index 0000000..0655962
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/generators.json
@@ -0,0 +1,16 @@
+{
+ "P1": "8929dfbc7e6642c4ed9cba0856e493f8b9d7d5fcb0c31ef8fdcd34d50648a56c795e106e9eada6e0bda386b414150755",
+ "Q1": "a9d40131066399fd41af51d883f4473b0dcd7d028d3d34ef17f3241d204e28507d7ecae032afa1d5490849b7678ec1f8",
+ "MsgGenerators": [
+ "903c7ca0b7e78a2017d0baf74103bd00ca8ff9bf429f834f071c75ffe6bfdec6d6dca15417e4ac08ca4ae1e78b7adc0e",
+ "84321f5855bfb6b001f0dfcb47ac9b5cc68f1a4edd20f0ec850e0563b27d2accee6edff1a26b357762fb24e8ddbb6fcb",
+ "b3060dff0d12a32819e08da00e61810676cc9185fdd750e5ef82b1a9798c7d76d63de3b6225d6c9a479d6c21a7c8bf93",
+ "8f1093d1e553cdead3c70ce55b6d664e5d1912cc9edfdd37bf1dad11ca396a0a8bb062092d391ebf8790ea5722413f68",
+ "990824e00b48a68c3d9a308e8c52a57b1bc84d1cf5d3c0f8c6fb6b1230e4e5b8eb752fb374da0b1ef687040024868140",
+ "b86d1c6ab8ce22bc53f625d1ce9796657f18060fcb1893ce8931156ef992fe56856199f8fa6c998e5d855a354a26b0dd",
+ "b4cdd98c5c1e64cb324e0c57954f719d5c5f9e8d991fd8e159b31c8d079c76a67321a30311975c706578d3a0ddc313b7",
+ "8311492d43ec9182a5fc44a75419b09547e311251fe38b6864dc1e706e29446cb3ea4d501634eb13327245fd8a574f77",
+ "ac00b493f92d17837a28d1f5b07991ca5ab9f370ae40d4f9b9f2711749ca200110ce6517dc28400d4ea25dddc146cacc",
+ "965a6c62451d4be6cb175dec39727dc665762673ee42bf0ac13a37a74784fbd61e84e0915277a6f59863b2bb4f5f6005"
+ ]
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/h2s.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/h2s.json
new file mode 100644
index 0000000..c21054f
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/h2s.json
@@ -0,0 +1,6 @@
+{
+ "caseName": "Hash to scalar output",
+ "message": "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "dst": "4242535f424c53313233383147315f584f463a5348414b452d3235365f535357555f524f5f4832475f484d32535f4832535f",
+ "scalar": "0500031f786fde5326aa9370dd7ffe9535ec7a52cf2b8f432cad5d9acfb73cd3"
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/keypair.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/keypair.json
new file mode 100644
index 0000000..5016524
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/keypair.json
@@ -0,0 +1,10 @@
+{
+ "caseName": "key pair fixture",
+ "keyMaterial": "746869732d49532d6a7573742d616e2d546573742d494b4d2d746f2d67656e65726174652d246528724074232d6b6579",
+ "keyInfo": "746869732d49532d736f6d652d6b65792d6d657461646174612d746f2d62652d757365642d696e2d746573742d6b65792d67656e",
+ "keyDst": "4242535f424c53313233383147315f584f463a5348414b452d3235365f535357555f524f5f4832475f484d32535f4b455947454e5f4453545f",
+ "keyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/mockedRng.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/mockedRng.json
new file mode 100644
index 0000000..ebb2305
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/mockedRng.json
@@ -0,0 +1,18 @@
+{
+ "caseName": "mocked random scalars",
+ "seed": "332e313431353932363533353839373933323338343632363433333833323739",
+ "dst": "4242535f424c53313233383147315f584f463a5348414b452d3235365f535357555f524f5f4832475f484d32535f4d4f434b5f52414e444f4d5f5343414c4152535f4453545f",
+ "count": 10,
+ "mockedScalars": [
+ "1004262112c3eaa95941b2b0d1311c09c845db0099a50e67eda628ad26b43083",
+ "6da7f145a94c1fa7f116b2482d59e4d466fe49c955ae8726e79453065156a9a4",
+ "05017919b3607e78c51e8ec34329955d49c8c90e4488079c43e74824e98f1306",
+ "4d451dad519b6a226bba79e11b44c441f1a74800eecfec6a2e2d79ea65b9d32d",
+ "5e7e4894e6dbe68023bc92ef15c410b01f3828109fc72b3b5ab159fc427b3f51",
+ "646e3014f49accb375253d268eb6c7f3289a1510f1e9452b612dd73a06ec5dd4",
+ "363ecc4c1f9d6d9144374de8f1f7991405e3345a3ec49dd485a39982753c11a4",
+ "12e592fe28d91d7b92a198c29afaa9d5329a4dcfdaf8b08557807412faeb4ac6",
+ "513325acdcdec7ea572360587b350a8b095ca19bdd8258c5c69d375e8706141a",
+ "6474fceba35e7e17365dde1a0284170180e446ae96c82943290d7baa3a6ed429"
+ ]
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof001.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof001.json
new file mode 100644
index 0000000..6636632
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof001.json
@@ -0,0 +1,34 @@
+{
+ "caseName": "valid single message signature, single-message revealed proof",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "b9a622a4b404e6ca4c85c15739d2124a1deb16df750be202e2430e169bc27fb71c44d98e6d40792033e1c452145ada95030832c5dc778334f2f1b528eced21b0b97a12025a283d78b7136bb9825d04ef",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"
+ ],
+ "disclosedIndexes": [
+ 0
+ ],
+ "proof": "89e4ab0c160880e0c2f12a754b9c051ed7f5fccfee3d5cbbb62e1239709196c737fff4303054660f8fcd08267a5de668a2e395ebe8866bdcb0dff9786d7014fa5e3c8cf7b41f8d7510e27d307f18032f6b788e200b9d6509f40ce1d2f962ceedb023d58ee44d660434e6ba60ed0da1a5d2cde031b483684cd7c5b13295a82f57e209b584e8fe894bcc964117bf3521b43d8e2eb59ce31f34d68b39f05bb2c625e4de5e61e95ff38bfd62ab07105d016414b45b01625c69965ad3c8a933e7b25d93daeb777302b966079827a99178240e6c3f13b7db2fb1f14790940e239d775ab32f539bdf9f9b582b250b05882996832652f7f5d3b6e04744c73ada1702d6791940ccbd75e719537f7ace6ee817298d",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "1308e6f945f663b96de1c76461cf7d7f88b92eb99a9034685150db443d733881",
+ "r2": "25f81cb69a8fac6fb55d44a084557258575d1003be2bd94f1922dad2c3e447fd",
+ "e_tilde": "5e8041a7ab02976ee50226c4b062b47d38829bbf42ee7eb899b29720377a584c",
+ "r1_tilde": "3bbf1d5dc2904dbb7b2ba75c5dce8a5ad2d56a359c13ff0fa5fcb1339cd2fe58",
+ "r3_tilde": "016b1460eee7707c524a86a4aedeb826ce9597b42906dccaa96c6b49a8ea7da2",
+ "m_tilde_scalars": []
+ },
+ "A_bar": "89e4ab0c160880e0c2f12a754b9c051ed7f5fccfee3d5cbbb62e1239709196c737fff4303054660f8fcd08267a5de668",
+ "B_bar": "a2e395ebe8866bdcb0dff9786d7014fa5e3c8cf7b41f8d7510e27d307f18032f6b788e200b9d6509f40ce1d2f962ceed",
+ "D": "b023d58ee44d660434e6ba60ed0da1a5d2cde031b483684cd7c5b13295a82f57e209b584e8fe894bcc964117bf3521b4",
+ "T1": "91a10e73cf4090812e8ea25f31aaa61be53fcb42ce86e9f0e5df6f6dac4c3eee62ac846b0b83a5cfcbe78315175a4961",
+ "T2": "988f3d473186634e41478dc4527cf240e64de23a763037454d39a876862ebc617738ba6c458142e3746b01eab58ca8d7",
+ "domain": "2f18dd269c11c512256a9d1d57e61a7d2de6ebcf41cac3053f37afedc4e650a9",
+ "challenge": "2652f7f5d3b6e04744c73ada1702d6791940ccbd75e719537f7ace6ee817298d"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof002.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof002.json
new file mode 100644
index 0000000..30270cd
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof002.json
@@ -0,0 +1,52 @@
+{
+ "caseName": "valid multi-message signature, all messages revealed proof",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9
+ ],
+ "proof": "91b0f598268c57b67bc9e55327c3c2b9b1654be89a0cf963ab392fa9e1637c565241d71fd6d7bbd7dfe243de85a9bac8b7461575c1e13b5055fed0b51fd0ec1433096607755b2f2f9ba6dc614dfa456916ca0d7fc6482b39c679cfb747a50ea1b3dd7ed57aaadc348361e2501a17317352e555a333e014e8e7d71eef808ae4f8fbdf45cd19fde45038bb310d5135f5205fc550b077e381fb3a3543dca31a0d8bba97bc0b660a5aa239eb74921e184aa3035fa01eaba32f52029319ec3df4fa4a4f716edb31a6ce19a19dbb971380099345070bd0fdeecf7c4774a33e0a116e069d5e215992fb637984802066dee6919146ae50b70ea52332dfe57f6e05c66e99f1764d8b890d121d65bfcc2984886ee0",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "1308e6f945f663b96de1c76461cf7d7f88b92eb99a9034685150db443d733881",
+ "r2": "25f81cb69a8fac6fb55d44a084557258575d1003be2bd94f1922dad2c3e447fd",
+ "e_tilde": "5e8041a7ab02976ee50226c4b062b47d38829bbf42ee7eb899b29720377a584c",
+ "r1_tilde": "3bbf1d5dc2904dbb7b2ba75c5dce8a5ad2d56a359c13ff0fa5fcb1339cd2fe58",
+ "r3_tilde": "016b1460eee7707c524a86a4aedeb826ce9597b42906dccaa96c6b49a8ea7da2",
+ "m_tilde_scalars": []
+ },
+ "A_bar": "91b0f598268c57b67bc9e55327c3c2b9b1654be89a0cf963ab392fa9e1637c565241d71fd6d7bbd7dfe243de85a9bac8",
+ "B_bar": "b7461575c1e13b5055fed0b51fd0ec1433096607755b2f2f9ba6dc614dfa456916ca0d7fc6482b39c679cfb747a50ea1",
+ "D": "b3dd7ed57aaadc348361e2501a17317352e555a333e014e8e7d71eef808ae4f8fbdf45cd19fde45038bb310d5135f520",
+ "T1": "8890adfc78da24768d59dbfdb3f380e2793e9018b20c23e9ba05baa60f1b21456bc047a5d27049dab5dc6a94696ce711",
+ "T2": "a49f953636d3651a3ae6fe45a99a2e4fec079eef3be8b8a6a4ba70885d7e028642f7224e9f451529915c88a7edc59fbe",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "46ae50b70ea52332dfe57f6e05c66e99f1764d8b890d121d65bfcc2984886ee0"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof003.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof003.json
new file mode 100644
index 0000000..99384c3
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof003.json
@@ -0,0 +1,53 @@
+{
+ "caseName": "valid multi-message signature, multiple messages revealed proof",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof004.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof004.json
new file mode 100644
index 0000000..d664696
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof004.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (different presentation header)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "011594ba7f95b3b470ea4102dd5899de3a042e5104d3ea01d15e6780d831d2be",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "different presentation header"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof005.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof005.json
new file mode 100644
index 0000000..43ddedb
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof005.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (wrong public key)",
+ "signerPublicKey": "b24c723803f84e210f7a95f6265c5cbfa4ecc51488bf7acf24b921807801c0798b725b9a2dcfa29953efcdfef03328720196c78b2e613727fd6e085302a0cc2d8d7e1d820cf1d36b20e79eee78c13a1a5da51a298f1aef86f07bc33388f089d8",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "wrong public key"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof006.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof006.json
new file mode 100644
index 0000000..68c03b5
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof006.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (modified messages)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "b3e4043a1e148028b85dfbf39d1e44d7bfc8277fd310aeda5deb4a6eb7b3d1293c86788288e86b1819caa0b11a4f2c6330abda72b1bcb082d660dc78b5271f6a047bb96c250f2ca877cc72464d363c3bd0bfc4d4b4de7233419234e94f16ec24359e13b6",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "modified messages"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof007.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof007.json
new file mode 100644
index 0000000..60ed2e0
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof007.json
@@ -0,0 +1,56 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (extra message un-revealed in proof)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ "",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 9
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "extra message un-revealed in proof"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof008.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof008.json
new file mode 100644
index 0000000..af4916a
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof008.json
@@ -0,0 +1,56 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (extra message invalid message un-revealed in proof)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ "",
+ "96012096"
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 9
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "extra message invalid message un-revealed in proof"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof009.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof009.json
new file mode 100644
index 0000000..3e33a95
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof009.json
@@ -0,0 +1,52 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (missing message revealed in proof)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "missing message revealed in proof"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof010.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof010.json
new file mode 100644
index 0000000..96499ab
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof010.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (re-ordered messages)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 4,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "re-ordered messages"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof011.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof011.json
new file mode 100644
index 0000000..dc3623e
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof011.json
@@ -0,0 +1,56 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (extra valid message, modified total message count)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ "",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6,
+ 9
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "extra valid message, modified total message count"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof012.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof012.json
new file mode 100644
index 0000000..996f597
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof012.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (truncated proof, one less undisclosed message)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d1",
+ "result": {
+ "valid": false,
+ "reason": "truncated proof, one less undisclosed message"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof013.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof013.json
new file mode 100644
index 0000000..4e310e6
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof013.json
@@ -0,0 +1,54 @@
+{
+ "caseName": "invalid multi-message signature, all messages revealed proof (different header)",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "ffeeddccbbaa00998877665544332211",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af356dd39bf8bcbfd41bf95d913f4c9b2979e1ed2ca10ac7e881bb6a271722549681e398d29e9ba4eac8848b168eddd5e4acec7df4103e2ed165e6e32edc80f0a3b28c36fb39ca19b4b8acee570deadba2da9ec20d1f236b571e0d4c2ea3b826fe924175ed4dfffbf18a9cfa98546c241efb9164c444d970e8c89849bc8601e96cf228fdefe38ab3b7e289cac859e68d9cbb0e648faf692b27df5ff6539c30da17e5444a65143de02ca64cee7b0823be65865cdc310be038ec6b594b99280072ae067bad1117b0ff3201a5506a8533b925c7ffae9cdb64558857db0ac5f5e0f18e750ae77ec9cf35263474fef3f78138c7a1ef5cfbc878975458239824fad3ce05326ba3969b1f5451bd82bd1f8075f3d32ece2d61d89a064ab4804c3c892d651d11bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558",
+ "result": {
+ "valid": false,
+ "reason": "different header"
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "1bc325464a71cd7aacc2d956a811aaff13ea4c35cef7842b656e8ba4758e7558"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof014.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof014.json
new file mode 100644
index 0000000..8a636f9
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof014.json
@@ -0,0 +1,53 @@
+{
+ "caseName": "valid multi-message signature, multiple messages revealed proof, no header",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "88beeb970f803160d3058eacde505207c576a8c9e4e5dc7c5249cbcf2a046c15f8df047031eef3436e04b779d92a9cdb1fe4c6cc035ba1634f1740f9dd49816d3ca745ecbe39f655ea61fb700137fded",
+ "header": "",
+ "presentationHeader": "bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "8ac336eea1d278656372d9914483c3d3b3069dfa4a7862293ac021dfeeebca93cadd7eb2b818f7b89719cdeffa5aa85989a7d691be11b1929a2bf089bfe9f2adc2c06788edc30585546efb74877f34ad91f0d6923b4ed7a53c49051dda8d056a95644ee738810772d90c1033f1dfe45c0b1b453d131170aafa8a99f812f3b90a5d1d9e6bd05a4dee6a50dd277ffc646f2429372f3ad9d5946ffeb53f24d41ffcc83c32cbb68afc9b6e0b64eebd24c69c6a7bd3bca8a6394ed8ae315abd555a6996f34d9da7680447947b3f35f54c38b562e990ee4d17a21569af4fc02f2991e6db78cc32d3ef9f6069fc5c2d47c8d8ff116dfb8a59641641961b854427f67649df14ab6e63f2d0d2a0cba2b2e1e835d20cd45e41f274532e9d50f31a690e5fef1c1456b65c668b80d8ec17b09bd5fb3b2c4edd6d6f5f790a5d6da22eb9a1aa2196d1a607f3c753813ba2bc6ece15d35263218fc7667c5f0fabfffe74745a8000e0415c8dafd5654ce6850ac2c6485d02433fdaebd9993f8b86a2eebb3beb10b4cc7735330384a3f4dfd4d5b21998ad0227b37e736cf9c144a0386f28cccf27a01e50aab45dda8275eb877728e77d2055309dba8c6604e7cff0d2c46ce6026b8e232c192955f909da6e47c2130c7e3f4f",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "8ac336eea1d278656372d9914483c3d3b3069dfa4a7862293ac021dfeeebca93cadd7eb2b818f7b89719cdeffa5aa859",
+ "B_bar": "89a7d691be11b1929a2bf089bfe9f2adc2c06788edc30585546efb74877f34ad91f0d6923b4ed7a53c49051dda8d056a",
+ "D": "95644ee738810772d90c1033f1dfe45c0b1b453d131170aafa8a99f812f3b90a5d1d9e6bd05a4dee6a50dd277ffc646f",
+ "T1": "a5405cc2c5965dda18714ab35f4d4a7ae4024f388fa7a5ba71202d4455b50b316ec37b360659e3012234562fa8989980",
+ "T2": "9827a40454cdc90a70e9c927f097019dbdd84768babb10ebcb460c2d918e1ce1c0512bf2cc49ed7ec476dfcde7a6a10c",
+ "domain": "333d8686761cff65a3a2ef20bfa217d37bdf19105e87c210e9ce64ea1210a157",
+ "challenge": "309dba8c6604e7cff0d2c46ce6026b8e232c192955f909da6e47c2130c7e3f4f"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof015.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof015.json
new file mode 100644
index 0000000..73e5a66
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/proof/proof015.json
@@ -0,0 +1,53 @@
+{
+ "caseName": "valid multi-message signature, multiple messages revealed proof, no presentation header",
+ "signerPublicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5",
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "header": "11223344556677889900aabbccddeeff",
+ "presentationHeader": "",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "disclosedIndexes": [
+ 0,
+ 2,
+ 4,
+ 6
+ ],
+ "proof": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af33fda9e14ba4cc0fcad8015bce3fecc4704799bef9924ab19688fc04f760c4da35017072a3e295788eff1b0dc2311bb199c186f86ea0540379d5a2ac8b7bd02d22487f2acc0e299115e16097b970badea802752a6fcb56cfbbcc2569916a8d3fe6d2d0fb1ae801cfc5ce056699adf23e3cd16b1fdf197deac099ab093da049a5b4451d038c71b7cc69e8390967594f6777a855c7f5d301f0f0573211ac85e2e165ea196f78c33f54092645a51341b777f0f5342301991f3da276c04b0224f7308090ae0b290d428a0570a71605a27977e7daf01d42dfbdcec252686c3060a73d81f6e151e23e3df2473b322da389f15a55cb2cd8a2bf29ef0d83d4876117735465fae956d8df56ec9eb0e4748ad3ef5587797368c51a0ccd67eb6da38602a1c2d4fd411214efc6932334ba0bcbf562626e7c0e1ae0db912c28d99f194fa3cd3a2",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "random_scalars": {
+ "r1": "5ee9426ae206e3a127eb53c79044bc9ed1b71354f8354b01bf410a02220be7d0",
+ "r2": "280d4fcc38376193ffc777b68459ed7ba897e2857f938581acf95ae5a68988f3",
+ "e_tilde": "39966b00042fc43906297d692ebb41de08e36aada8d9504d4e0ae02ad59e9230",
+ "r1_tilde": "61f5c273999b0b50be8f84d2380eb9220fc5a88afe144efc4007545f0ab9c089",
+ "r3_tilde": "63af117e0c8b7d2f1f3e375fcf5d9430e136ff0f7e879423e49dadc401a50089",
+ "m_tilde_scalars": [
+ "020b83ca2ab319cba0744d6d58da75ac3dfb6ba682bfce2587c5a6d86a4e4e7b",
+ "5bf565343611c08f83e4420e8b1577ace8cc4df5d5303aeb3c4e425f1080f836",
+ "049d77949af1192534da28975f76d4f211315dce1e36f93ffcf2a555de516b28",
+ "407e5a952f145de7da53533de8366bbd2e0c854721a204f03906dc82fde10f48",
+ "1c925d9052849edddcf04d5f1f0d4ff183a66b66eb820f59b675aee121cfc63c",
+ "07d7c41b02158a9c5eac212ed6d7c2cddeb8e38baea6e93e1a00b2e83e2a0995"
+ ]
+ },
+ "A_bar": "b1f8bf99a11c39f04e2a032183c1ead12956ad322dd06799c50f20fb8cf6b0ac279210ef5a2920a7be3ec2aa0911ace7",
+ "B_bar": "b96811a98f3c1cceba4a2147ae763b3ba036f47bc21c39179f2b395e0ab1ac49017ea5b27848547bedd27be481c1dfc0",
+ "D": "b73372346feb94ab16189d4c525652b8d3361bab43463700720ecfb0ee75e595ea1b13330615011050a0dfcffdb21af3",
+ "T1": "8b497dd4dcdcf7eb58c9b43e57e06bcea3468a223ae2fc015d7a86506a952d68055e73f5a5847e58f133ea154256d0da",
+ "T2": "8655584d3da1313f881f48c239384a5623d2d292f08dae7ac1d8129c19a02a89b82fa45de3f6c2c439510fce5919656f",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b",
+ "challenge": "4fd411214efc6932334ba0bcbf562626e7c0e1ae0db912c28d99f194fa3cd3a2"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature001.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature001.json
new file mode 100644
index 0000000..77ace19
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature001.json
@@ -0,0 +1,19 @@
+{
+ "caseName": "valid single message signature",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"
+ ],
+ "signature": "b9a622a4b404e6ca4c85c15739d2124a1deb16df750be202e2430e169bc27fb71c44d98e6d40792033e1c452145ada95030832c5dc778334f2f1b528eced21b0b97a12025a283d78b7136bb9825d04ef",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "B": "8bbc8c123d3f128f206dd0d2dae490e82af08b84e8d70af3dc291d32a6e98f635beefcc4533b2599804a164aabe68d7c",
+ "domain": "2f18dd269c11c512256a9d1d57e61a7d2de6ebcf41cac3053f37afedc4e650a9"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature002.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature002.json
new file mode 100644
index 0000000..948574f
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature002.json
@@ -0,0 +1,20 @@
+{
+ "caseName": "invalid single message signature (modified message)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ ""
+ ],
+ "signature": "b9a622a4b404e6ca4c85c15739d2124a1deb16df750be202e2430e169bc27fb71c44d98e6d40792033e1c452145ada95030832c5dc778334f2f1b528eced21b0b97a12025a283d78b7136bb9825d04ef",
+ "result": {
+ "valid": false,
+ "reason": "modified message"
+ },
+ "trace": {
+ "B": "8bbc8c123d3f128f206dd0d2dae490e82af08b84e8d70af3dc291d32a6e98f635beefcc4533b2599804a164aabe68d7c",
+ "domain": "2f18dd269c11c512256a9d1d57e61a7d2de6ebcf41cac3053f37afedc4e650a9"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature003.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature003.json
new file mode 100644
index 0000000..1bb7d60
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature003.json
@@ -0,0 +1,21 @@
+{
+ "caseName": "invalid single message signature (extra unsigned message)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"
+ ],
+ "signature": "b9a622a4b404e6ca4c85c15739d2124a1deb16df750be202e2430e169bc27fb71c44d98e6d40792033e1c452145ada95030832c5dc778334f2f1b528eced21b0b97a12025a283d78b7136bb9825d04ef",
+ "result": {
+ "valid": false,
+ "reason": "extra unsigned message"
+ },
+ "trace": {
+ "B": "8bbc8c123d3f128f206dd0d2dae490e82af08b84e8d70af3dc291d32a6e98f635beefcc4533b2599804a164aabe68d7c",
+ "domain": "2f18dd269c11c512256a9d1d57e61a7d2de6ebcf41cac3053f37afedc4e650a9"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature004.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature004.json
new file mode 100644
index 0000000..e3b3be5
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature004.json
@@ -0,0 +1,28 @@
+{
+ "caseName": "valid multi-message signature",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "B": "ae8d4ebe248b9ad9c933d5661bfb46c56721fba2a1182ddda7e8fb443bda3c0a571ad018ad31d0b6d1f4e8b985e6c58d",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature005.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature005.json
new file mode 100644
index 0000000..f29df89
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature005.json
@@ -0,0 +1,21 @@
+{
+ "caseName": "invalid multi-message signature (missing messages)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"
+ ],
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "result": {
+ "valid": false,
+ "reason": "missing messages"
+ },
+ "trace": {
+ "B": "ae8d4ebe248b9ad9c933d5661bfb46c56721fba2a1182ddda7e8fb443bda3c0a571ad018ad31d0b6d1f4e8b985e6c58d",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature006.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature006.json
new file mode 100644
index 0000000..02e02db
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature006.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (re-ordered messages)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "",
+ "96012096",
+ "ac55fb33a75909ed",
+ "d183ddc6e2665aa4e2f088af",
+ "515ae153e22aae04ad16f759e07237b4",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"
+ ],
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "result": {
+ "valid": false,
+ "reason": "re-ordered messages"
+ },
+ "trace": {
+ "B": "ae8d4ebe248b9ad9c933d5661bfb46c56721fba2a1182ddda7e8fb443bda3c0a571ad018ad31d0b6d1f4e8b985e6c58d",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature007.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature007.json
new file mode 100644
index 0000000..dbb55c3
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature007.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (wrong public key)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "b24c723803f84e210f7a95f6265c5cbfa4ecc51488bf7acf24b921807801c0798b725b9a2dcfa29953efcdfef03328720196c78b2e613727fd6e085302a0cc2d8d7e1d820cf1d36b20e79eee78c13a1a5da51a298f1aef86f07bc33388f089d8"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "result": {
+ "valid": false,
+ "reason": "wrong public key"
+ },
+ "trace": {
+ "B": "ae8d4ebe248b9ad9c933d5661bfb46c56721fba2a1182ddda7e8fb443bda3c0a571ad018ad31d0b6d1f4e8b985e6c58d",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature008.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature008.json
new file mode 100644
index 0000000..b409db4
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature008.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (different header)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "ffeeddccbbaa00998877665544332211",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "result": {
+ "valid": false,
+ "reason": "different header"
+ },
+ "trace": {
+ "B": "ae8d4ebe248b9ad9c933d5661bfb46c56721fba2a1182ddda7e8fb443bda3c0a571ad018ad31d0b6d1f4e8b985e6c58d",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature009.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature009.json
new file mode 100644
index 0000000..fc43ec5
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature009.json
@@ -0,0 +1,29 @@
+{
+ "caseName": "invalid multi-message signature (re-ordered(randomly shuffled) messages)",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "11223344556677889900aabbccddeeff",
+ "messages": [
+ "",
+ "96012096",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "ac55fb33a75909ed",
+ "d183ddc6e2665aa4e2f088af",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "515ae153e22aae04ad16f759e07237b4"
+ ],
+ "signature": "956a3427b1b8e3642e60e6a7990b67626811adeec7a0a6cb4f770cdd7c20cf08faabb913ac94d18e1e92832e924cb6e202912b624261fc6c59b0fea801547f67fb7d3253e1e2acbcf90ef59a6911931e",
+ "result": {
+ "valid": false,
+ "reason": "re-ordered(randomly shuffled) messages"
+ },
+ "trace": {
+ "B": "ae8d4ebe248b9ad9c933d5661bfb46c56721fba2a1182ddda7e8fb443bda3c0a571ad018ad31d0b6d1f4e8b985e6c58d",
+ "domain": "6f7ee8de30835599bb540d2cb4dd02fd0c6cf8246f14c9ee9a8463f7fd400f7b"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature010.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature010.json
new file mode 100644
index 0000000..c099757
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/bls12-381-shake-256/signature/signature010.json
@@ -0,0 +1,28 @@
+{
+ "caseName": "valid multi-message signature, no header",
+ "signerKeyPair": {
+ "secretKey": "2eee0f60a8a3a8bec0ee942bfd46cbdae9a0738ee68f5a64e7238311cf09a079",
+ "publicKey": "92d37d1d6cd38fea3a873953333eab23a4c0377e3e049974eb62bd45949cdeb18fb0490edcd4429adff56e65cbce42cf188b31bddbd619e419b99c2c41b38179eb001963bc3decaae0d9f702c7a8c004f207f46c734a5eae2e8e82833f3e7ea5"
+ },
+ "header": "",
+ "messages": [
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+ ],
+ "signature": "88beeb970f803160d3058eacde505207c576a8c9e4e5dc7c5249cbcf2a046c15f8df047031eef3436e04b779d92a9cdb1fe4c6cc035ba1634f1740f9dd49816d3ca745ecbe39f655ea61fb700137fded",
+ "result": {
+ "valid": true
+ },
+ "trace": {
+ "B": "8607ebc413b397c1e27ce591d1daa39f73da329018bda0f90bf996355cc28c3cdba19feeb81e35be9e1503a018e4086e",
+ "domain": "333d8686761cff65a3a2ef20bfa217d37bdf19105e87c210e9ce64ea1210a157"
+ }
+}
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/messages.json b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/messages.json
new file mode 100644
index 0000000..b674216
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/official/messages.json
@@ -0,0 +1,12 @@
+[
+ "9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02",
+ "c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80",
+ "7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73",
+ "77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c",
+ "496694774c5604ab1b2544eababcf0f53278ff50",
+ "515ae153e22aae04ad16f759e07237b4",
+ "d183ddc6e2665aa4e2f088af",
+ "ac55fb33a75909ed",
+ "96012096",
+ ""
+]
\ No newline at end of file
diff --git a/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/sha256.properties b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/sha256.properties
new file mode 100644
index 0000000..540435c
--- /dev/null
+++ b/zeroj-bbs/src/test/resources/cfrg-bbs/draft10/sha256.properties
@@ -0,0 +1,24 @@
+# CFRG BBS draft-irtf-cfrg-bbs-signatures-10, Section 8.4 BLS12-381 SHA-256 vectors.
+ciphersuite_id=BBS_BLS12381G1_XMD:SHA-256_SSWU_RO_
+key_material=746869732d49532d6a7573742d616e2d546573742d494b4d2d746f2d67656e65726174652d246528724074232d6b6579
+key_info=746869732d49532d736f6d652d6b65792d6d657461646174612d746f2d62652d757365642d696e2d746573742d6b65792d67656e
+sk=60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc
+pk=a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c
+header=11223344556677889900aabbccddeeff
+presentation_header=bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501
+messages=9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02,c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80,7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73,77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c,496694774c5604ab1b2544eababcf0f53278ff50,515ae153e22aae04ad16f759e07237b4,d183ddc6e2665aa4e2f088af,ac55fb33a75909ed,96012096,
+message_scalars=1cb5bb86114b34dc438a911617655a1db595abafac92f47c5001799cf624b430,154249d503c093ac2df516d4bb88b510d54fd97e8d7121aede420a25d9521952,0c7c4c85cdab32e6fdb0de267b16fa3212733d4e3a3f0d0f751657578b26fe22,4a196deafee5c23f630156ae13be3e46e53b7e39094d22877b8cba7f14640888,34c5ea4f2ba49117015a02c711bb173c11b06b3f1571b88a2952b93d0ed4cf7e,4045b39b83055cd57a4d0203e1660800fabe434004dbdc8730c21ce3f0048b08,064621da4377b6b1d05ecc37cf3b9dfc94b9498d7013dc5c4a82bf3bb1750743,34ac9196ace0a37e147e32319ea9b3d8cc7d21870d3c3ba071246859cca49b02,57eb93f417c43200e9784fa5ea5a59168d3dbc38df707a13bb597c871b2a5f74,08e3afeb2b4f2b5f907924ef42856616e6f2d5f1fb373736db1cca32707a7d16
+generators=a9ec65b70a7fbe40c874c9eb041c2cb0a7af36ccec1bea48fa2ba4c2eb67ef7f9ecb17ed27d38d27cdeddff44c8137be,98cd5313283aaf5db1b3ba8611fe6070d19e605de4078c38df36019fbaad0bd28dd090fd24ed27f7f4d22d5ff5dea7d4,a31fbe20c5c135bcaa8d9fc4e4ac665cc6db0226f35e737507e803044093f37697a9d452490a970eea6f9ad6c3dcaa3a,b479263445f4d2108965a9086f9d1fdc8cde77d14a91c856769521ad3344754cc5ce90d9bc4c696dffbc9ef1d6ad1b62,ac0401766d2128d4791d922557c7b4d1ae9a9b508ce266575244a8d6f32110d7b0b7557b77604869633bb49afbe20035,b95d2898370ebc542857746a316ce32fa5151c31f9b57915e308ee9d1de7db69127d919e984ea0747f5223821b596335,8f19359ae6ee508157492c06765b7df09e2e5ad591115742f2de9c08572bb2845cbf03fd7e23b7f031ed9c7564e52f39,abc914abe2926324b2c848e8a411a2b6df18cbe7758db8644145fefb0bf0a2d558a8c9946bd35e00c69d167aadf304c1,80755b3eb0dd4249cbefd20f177cee88e0761c066b71794825c9997b551f24051c352567ba6c01e57ac75dff763eaa17,82701eb98070728e1769525e73abff1783cedc364adb20c05c897a62f2ab2927f86f118dcb7819a7b218d8f3fee4bd7f,a1f229540474f4d6f1134761b92b788128c7ac8dc9b0c52d59493132679673032ac7db3fb3d79b46b13c1c41ee495bca
+single_message_b=92d264aed02bf23de022ebe778c4f929fddf829f504e451d011ed89a313b8167ac947332e1648157ceffc6e6e41ab255
+single_message_domain=25d57fab92a8274c68fde5c3f16d4b275e4a156f211ae34b3ab32fbaf506ed5c
+single_message_signature=84773160b824e194073a57493dac1a20b667af70cd2352d8af241c77658da5253aa8458317cca0eae615690d55b1f27164657dcafee1d5c1973947aa70e2cfbb4c892340be5969920d0916067b4565a0
+multi_message_b=84f48376f7df6af40bc329cf484cdbfd0b19d0b326fccab4e9d8f00d1dbcf48139d498b19667f203cf8a1d1f8340c522
+multi_message_domain=6272832582a0ac96e6fe53e879422f24c51680b25fbf17bad22a35ea93ce5b47
+multi_message_signature=8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8
+mock_seed=332e313431353932363533353839373933323338343632363433333833323739
+mock_dst=4242535f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4832475f484d32535f4d4f434b5f52414e444f4d5f5343414c4152535f4453545f
+mock_scalars=04f8e2518993c4383957ad14eb13a023c4ad0c67d01ec86eeb902e732ed6df3f,5d87c1ba64c320ad601d227a1b74188a41a100325cecf00223729863966392b1,0444607600ac70482e9c983b4b063214080b9e808300aa4cc02a91b3a92858fe,548cd11eae4318e88cda10b4cd31ae29d41c3a0b057196ee9cf3a69d471e4e94,2264b06a08638b69b4627756a62f08e0dc4d8240c1b974c9c7db779a769892f4,4d99352986a9f8978b93485d21525244b21b396cf61f1d71f7c48e3fbc970a42,5ed8be91662386243a6771fbdd2c627de31a44220e8d6f745bad5d99821a4880,62ff1734b939ddd87beeb37a7bbcafa0a274cbc1b07384198f0e88398272208d,05c2a0af016df58e844db8944082dcaf434de1b1e2e7136ec8a99b939b716223,485e2adab17b76f5334c95bf36c03ccf91cef77dcfcdc6b8a69e2090b3156663
+single_proof_random_scalars=60ca409f6b0563f687fc471c63d2819f446f39c23bb540925d9d4254ac58f337,2ceff4982de0c913090f75f081df5ec594c310bb48c17cfdaab5332a682ef811,6101c4404895f3dff87ab39c34cb995af07e7139e6b3847180ffdd1bc8c313cd,0dfcffd97a6ecdebef3c9c114b99d7a030c998d938905f357df62822dee072e8,639e3417007d38e5d34ba8c511e836768ddc2669fdd3faff5c14ad27ac2b2da1
+single_proof=94916292a7a6bade28456c601d3af33fcf39278d6594b467e128a3f83686a104ef2b2fcf72df0215eeaf69262ffe8194a19fab31a82ddbe06908985abc4c9825788b8a1610942d12b7f5debbea8985296361206dbace7af0cc834c80f33e0aadaeea5597befbb651827b5eed5a66f1a959bb46cfd5ca1a817a14475960f69b32c54db7587b5ee3ab665fbd37b506830a49f21d592f5e634f47cee05a025a2f8f94e73a6c15f02301d1178a92873b6e8634bafe4983c3e15a663d64080678dbf29417519b78af042be2b3e1c4d08b8d520ffab008cbaaca5671a15b22c239b38e940cfeaa5e72104576a9ec4a6fad78c532381aeaa6fb56409cef56ee5c140d455feeb04426193c57086c9b6d397d9418
+all_disclosed_proof=b1f468aec2001c4f54cb56f707c6222a43e5803a25b2253e67b2210ab2ef9eab52db2d4b379935c4823281eaf767fd37b08ce80dc65de8f9769d27099ae649ad4c9b4bd2cc23edcba52073a298087d2495e6d57aaae051ef741adf1cbce65c64a73c8c97264177a76c4a03341956d2ae45ed3438ce598d5cda4f1bf9507fecef47855480b7b30b5e4052c92a4360110c67327365763f5aa9fb85ddcbc2975449b8c03db1216ca66b310f07d0ccf12ab460cdc6003b677fed36d0a23d0818a9d4d098d44f749e91008cf50e8567ef936704c8277b7710f41ab7e6e16408ab520edc290f9801349aee7b7b4e318e6a76e028e1dea911e2e7baec6a6a174da1a22362717fbae1cd961d7bf4adce1d31c2ab
+some_disclosed_proof=a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a
diff --git a/zeroj-bls12381-wasm/README.md b/zeroj-bls12381-wasm/README.md
new file mode 100644
index 0000000..f377691
--- /dev/null
+++ b/zeroj-bls12381-wasm/README.md
@@ -0,0 +1,25 @@
+# zeroj-bls12381-wasm
+
+Optional BLS12-381 provider backed by the Rust `bls12_381` crate compiled to
+`wasm32-unknown-unknown` and loaded through Chicory.
+
+## Reproducible build inputs
+
+- Rust is pinned by `rust/rust-toolchain.toml`.
+- The WASM target is declared in the pinned toolchain file.
+- Rust dependencies are locked by `rust/Cargo.lock`.
+- The generated `zeroj_bls12381.wasm` artifact is not checked in. Gradle builds
+ it from source and packages it as `zeroj-bls12381/zeroj_bls12381.wasm`.
+
+Build and test the module with:
+
+```bash
+./gradlew :zeroj-bls12381-wasm:test
+```
+
+Build only the Rust artifact with:
+
+```bash
+cd zeroj-bls12381-wasm/rust
+cargo build --release --target wasm32-unknown-unknown
+```
diff --git a/zeroj-bls12381-wasm/build.gradle b/zeroj-bls12381-wasm/build.gradle
new file mode 100644
index 0000000..b3e6b5d
--- /dev/null
+++ b/zeroj-bls12381-wasm/build.gradle
@@ -0,0 +1,51 @@
+plugins {
+ id 'java-library'
+}
+
+description = 'ZeroJ BLS12-381 WASM provider — zkcrypto Rust WASM via Chicory'
+
+def bls12381RustDir = project.file('rust')
+def bls12381WasmTarget = bls12381RustDir.toPath()
+ .resolve('target/wasm32-unknown-unknown/release/zeroj_bls12381.wasm')
+ .toFile()
+def generatedWasmDir = layout.buildDirectory.dir('generated-resources/wasm')
+
+dependencies {
+ api project(':zeroj-bls12381')
+
+ implementation 'com.dylibso.chicory:runtime:1.7.5'
+ implementation 'com.dylibso.chicory:wasm:1.7.5'
+}
+
+tasks.register('buildBls12381Wasm', Exec) {
+ description = 'Builds the zkcrypto BLS12-381 Rust crate as wasm32-unknown-unknown'
+ group = 'build'
+ workingDir = bls12381RustDir
+ commandLine 'cargo', 'build', '--release', '--target', 'wasm32-unknown-unknown'
+ inputs.files(fileTree(bls12381RustDir) {
+ include 'Cargo.toml', 'Cargo.lock', 'src/**/*.rs'
+ })
+ outputs.file(bls12381WasmTarget)
+}
+
+tasks.register('copyBls12381Wasm', Copy) {
+ dependsOn tasks.named('buildBls12381Wasm')
+ from(bls12381WasmTarget)
+ into(generatedWasmDir.map { it.dir('zeroj-bls12381') })
+}
+
+processResources {
+ dependsOn tasks.named('copyBls12381Wasm')
+ from(generatedWasmDir)
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ pom {
+ name = 'ZeroJ BLS12-381 WASM'
+ description = 'Optional BLS12-381 primitive provider using zkcrypto Rust WASM and Chicory'
+ }
+ }
+ }
+}
diff --git a/zeroj-bls12381-wasm/rust/Cargo.lock b/zeroj-bls12381-wasm/rust/Cargo.lock
new file mode 100644
index 0000000..c420645
--- /dev/null
+++ b/zeroj-bls12381-wasm/rust/Cargo.lock
@@ -0,0 +1,105 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[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 = "bls12_381"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403"
+dependencies = [
+ "ff",
+ "group",
+ "pairing",
+ "rand_core",
+ "subtle",
+]
+
+[[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 = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "pairing"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
+dependencies = [
+ "group",
+]
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zeroj-bls12381-wasm"
+version = "0.1.0"
+dependencies = [
+ "bls12_381",
+]
diff --git a/zeroj-bls12381-wasm/rust/Cargo.toml b/zeroj-bls12381-wasm/rust/Cargo.toml
new file mode 100644
index 0000000..35d6238
--- /dev/null
+++ b/zeroj-bls12381-wasm/rust/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "zeroj-bls12381-wasm"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+name = "zeroj_bls12381"
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+bls12_381 = "0.8.0"
+
+[profile.release]
+opt-level = "z"
+lto = true
+panic = "abort"
+codegen-units = 1
diff --git a/zeroj-bls12381-wasm/rust/rust-toolchain.toml b/zeroj-bls12381-wasm/rust/rust-toolchain.toml
new file mode 100644
index 0000000..6596900
--- /dev/null
+++ b/zeroj-bls12381-wasm/rust/rust-toolchain.toml
@@ -0,0 +1,4 @@
+[toolchain]
+channel = "1.94.0"
+targets = ["wasm32-unknown-unknown"]
+profile = "minimal"
diff --git a/zeroj-bls12381-wasm/rust/src/lib.rs b/zeroj-bls12381-wasm/rust/src/lib.rs
new file mode 100644
index 0000000..f6e86b8
--- /dev/null
+++ b/zeroj-bls12381-wasm/rust/src/lib.rs
@@ -0,0 +1,194 @@
+use bls12_381::{
+ multi_miller_loop, G1Affine, G2Affine, G2Prepared, Gt, Scalar,
+};
+use std::{mem, slice};
+
+const G1_BYTES: usize = 96;
+const G2_BYTES: usize = 192;
+const SCALAR_BYTES: usize = 32;
+
+#[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_bls12381_version() -> u32 {
+ 1
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bls12381_g1_generator() -> *mut u8 {
+ respond(Ok(G1Affine::generator().to_uncompressed().to_vec()))
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bls12381_g2_generator() -> *mut u8 {
+ respond(Ok(G2Affine::generator().to_uncompressed().to_vec()))
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bls12381_g1_scalar_mul(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, g1_scalar_mul)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bls12381_g2_scalar_mul(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, g2_scalar_mul)
+}
+
+#[no_mangle]
+pub extern "C" fn zeroj_bls12381_pairing_check(ptr: *const u8, len: usize) -> *mut u8 {
+ handle(ptr, len, pairing_check)
+}
+
+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
+}
+
+fn g1_scalar_mul(input: &[u8]) -> Result, String> {
+ require_len(input, G1_BYTES + SCALAR_BYTES, "G1 scalar multiplication request")?;
+ let point = read_g1(&input[..G1_BYTES])?;
+ let scalar = read_scalar(&input[G1_BYTES..])?;
+ let result = G1Affine::from(&point * &scalar);
+ Ok(result.to_uncompressed().to_vec())
+}
+
+fn g2_scalar_mul(input: &[u8]) -> Result, String> {
+ require_len(input, G2_BYTES + SCALAR_BYTES, "G2 scalar multiplication request")?;
+ let point = read_g2(&input[..G2_BYTES])?;
+ let scalar = read_scalar(&input[G2_BYTES..])?;
+ let result = G2Affine::from(&point * &scalar);
+ Ok(result.to_uncompressed().to_vec())
+}
+
+fn pairing_check(input: &[u8]) -> Result, String> {
+ if input.len() < 4 {
+ return Err("pairing check request is too short".into());
+ }
+ let count = u32::from_le_bytes(input[..4].try_into().unwrap()) as usize;
+ let pair_len = G1_BYTES + G2_BYTES;
+ let expected = 4usize
+ .checked_add(count.checked_mul(pair_len).ok_or("pairing request is too large")?)
+ .ok_or("pairing request is too large")?;
+ require_len(input, expected, "pairing check request")?;
+
+ let mut g1_points = Vec::with_capacity(count);
+ let mut g2_prepared = Vec::with_capacity(count);
+ let mut offset = 4;
+ for _ in 0..count {
+ g1_points.push(read_g1(&input[offset..offset + G1_BYTES])?);
+ offset += G1_BYTES;
+ let g2 = read_g2(&input[offset..offset + G2_BYTES])?;
+ offset += G2_BYTES;
+ g2_prepared.push(G2Prepared::from(g2));
+ }
+
+ let terms: Vec<_> = g1_points.iter().zip(g2_prepared.iter()).collect();
+ let is_identity = multi_miller_loop(&terms).final_exponentiation() == Gt::identity();
+ Ok(vec![if is_identity { 1 } else { 0 }])
+}
+
+fn read_g1(bytes: &[u8]) -> Result {
+ let arr: [u8; G1_BYTES] = bytes.try_into().map_err(|_| "invalid G1 byte length")?;
+ let point = G1Affine::from_uncompressed(&arr);
+ if bool::from(point.is_some()) {
+ Ok(point.unwrap())
+ } else {
+ Err("invalid G1 point encoding".into())
+ }
+}
+
+fn read_g2(bytes: &[u8]) -> Result {
+ let arr: [u8; G2_BYTES] = bytes.try_into().map_err(|_| "invalid G2 byte length")?;
+ let point = G2Affine::from_uncompressed(&arr);
+ if bool::from(point.is_some()) {
+ Ok(point.unwrap())
+ } else {
+ Err("invalid G2 point encoding".into())
+ }
+}
+
+fn read_scalar(bytes: &[u8]) -> Result {
+ let arr: [u8; SCALAR_BYTES] = bytes.try_into().map_err(|_| "invalid scalar byte length")?;
+ let scalar = Scalar::from_bytes(&arr);
+ if bool::from(scalar.is_some()) {
+ Ok(scalar.unwrap())
+ } else {
+ Err("invalid BLS12-381 scalar encoding".into())
+ }
+}
+
+fn require_len(input: &[u8], expected: usize, label: &str) -> Result<(), String> {
+ if input.len() != expected {
+ return Err(format!("{label} must be {expected} bytes, got {}", input.len()));
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn generator_pairing_is_not_identity() {
+ let g1 = G1Affine::generator();
+ let g2 = G2Prepared::from(G2Affine::generator());
+ let terms = vec![(&g1, &g2)];
+ assert_ne!(multi_miller_loop(&terms).final_exponentiation(), Gt::identity());
+ }
+
+ #[test]
+ fn generator_plus_neg_generator_pairing_is_identity() {
+ let g1 = G1Affine::generator();
+ let neg_g1 = -G1Affine::generator();
+ let g2 = G2Prepared::from(G2Affine::generator());
+ let terms = vec![(&g1, &g2), (&neg_g1, &g2)];
+ assert_eq!(multi_miller_loop(&terms).final_exponentiation(), Gt::identity());
+ }
+}
diff --git a/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClient.java b/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClient.java
new file mode 100644
index 0000000..1e223af
--- /dev/null
+++ b/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClient.java
@@ -0,0 +1,197 @@
+package com.bloxbean.cardano.zeroj.bls12381.wasm;
+
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.runtime.Memory;
+import com.dylibso.chicory.wasm.Parser;
+
+import java.io.IOException;
+import java.math.BigInteger;
+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.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Chicory client for the ZeroJ BLS12-381 Rust WASM module.
+ */
+public final class Bls12381WasmClient {
+ public static final String DEFAULT_RESOURCE = "/zeroj-bls12381/zeroj_bls12381.wasm";
+
+ private static final int MAX_RESPONSE_LEN = 16 * 1024 * 1024;
+
+ private final Instance instance;
+ private final Memory memory;
+
+ public Bls12381WasmClient(byte[] wasmBytes) {
+ Objects.requireNonNull(wasmBytes, "wasmBytes required");
+ if (wasmBytes.length == 0) {
+ throw new IllegalArgumentException("wasmBytes must not be empty");
+ }
+ try {
+ this.instance = Instance.builder(Parser.parse(wasmBytes)).build();
+ this.memory = Objects.requireNonNull(instance.memory(), "BLS12-381 WASM module must export memory");
+ long version = instance.export("zeroj_bls12381_version").apply()[0];
+ if (version != 1L) {
+ throw new Bls12381WasmException("Unsupported BLS12-381 WASM ABI version: " + version);
+ }
+ } catch (Bls12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bls12381WasmException("Failed to initialize BLS12-381 WASM module", e);
+ }
+ }
+
+ public static Bls12381WasmClient fromPath(Path wasmPath) throws IOException {
+ return new Bls12381WasmClient(Files.readAllBytes(wasmPath));
+ }
+
+ public static Bls12381WasmClient createDefault() {
+ try (var in = Bls12381WasmClient.class.getResourceAsStream(DEFAULT_RESOURCE)) {
+ if (in == null) {
+ throw new Bls12381WasmException("BLS12-381 WASM resource not found: " + DEFAULT_RESOURCE);
+ }
+ return new Bls12381WasmClient(in.readAllBytes());
+ } catch (IOException e) {
+ throw new Bls12381WasmException("Failed to read BLS12-381 WASM resource", e);
+ }
+ }
+
+ public G1Point g1Generator() {
+ return Bls12381Codecs.g1FromUncompressed(invokeNoArg("zeroj_bls12381_g1_generator"));
+ }
+
+ public G2Point g2Generator() {
+ return Bls12381Codecs.g2FromUncompressed(invokeNoArg("zeroj_bls12381_g2_generator"));
+ }
+
+ public G1Point g1ScalarMul(G1Point point, BigInteger scalar) {
+ byte[] request = concat(
+ Bls12381Codecs.g1ToUncompressed(Bls12381Codecs.requireValid(point)),
+ Bls12381Codecs.scalarToLittleEndian32Reduced(Objects.requireNonNull(scalar, "scalar required")));
+ return Bls12381Codecs.g1FromUncompressed(invoke("zeroj_bls12381_g1_scalar_mul", request));
+ }
+
+ public G2Point g2ScalarMul(G2Point point, BigInteger scalar) {
+ byte[] request = concat(
+ Bls12381Codecs.g2ToUncompressed(Bls12381Codecs.requireValid(point)),
+ Bls12381Codecs.scalarToLittleEndian32Reduced(Objects.requireNonNull(scalar, "scalar required")));
+ return Bls12381Codecs.g2FromUncompressed(invoke("zeroj_bls12381_g2_scalar_mul", request));
+ }
+
+ public boolean pairingProductIsIdentity(G1Point[] g1Points, G2Point[] g2Points) {
+ Objects.requireNonNull(g1Points, "g1Points required");
+ Objects.requireNonNull(g2Points, "g2Points required");
+ if (g1Points.length != g2Points.length) {
+ throw new IllegalArgumentException("Arrays must have equal length");
+ }
+ ByteBuffer request = ByteBuffer
+ .allocate(4 + g1Points.length * (Bls12381Codecs.G1_UNCOMPRESSED_BYTES + Bls12381Codecs.G2_UNCOMPRESSED_BYTES))
+ .order(ByteOrder.LITTLE_ENDIAN);
+ request.putInt(g1Points.length);
+ for (int i = 0; i < g1Points.length; i++) {
+ request.put(Bls12381Codecs.g1ToUncompressed(Bls12381Codecs.requireValid(g1Points[i])));
+ request.put(Bls12381Codecs.g2ToUncompressed(Bls12381Codecs.requireValid(g2Points[i])));
+ }
+ byte[] response = invoke("zeroj_bls12381_pairing_check", request.array());
+ if (response.length != 1) {
+ throw new Bls12381WasmException("Invalid BLS12-381 pairing response length: " + response.length);
+ }
+ return response[0] != 0;
+ }
+
+ 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];
+ }
+
+ 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(responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(exportName, responsePtr, (int) responseLen);
+ } catch (Bls12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bls12381WasmException("BLS12-381 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(responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(exportName, responsePtr, (int) responseLen);
+ } catch (Bls12381WasmException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new Bls12381WasmException("BLS12-381 WASM invocation failed: " + exportName, e);
+ } finally {
+ if (responsePtr != 0 && responseAllocationLen > 0) {
+ instance.export("dealloc").apply(responsePtr, responseAllocationLen);
+ }
+ }
+ }
+
+ private long readResponseLenHeader(int responsePtr) {
+ byte[] lenBytes = memory.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 Bls12381WasmException("Invalid BLS12-381 WASM response length: " + responseLen);
+ }
+ }
+
+ private long responseAllocationLen(long responseLen) {
+ long maxWasmAllocationLen = Integer.toUnsignedLong(-1);
+ return responseLen <= maxWasmAllocationLen - 4 ? responseLen + 4 : 0;
+ }
+
+ private byte[] readResponsePayload(String exportName, int responsePtr, int responseLen) {
+ byte[] response = memory.readBytes(responsePtr + 4, responseLen);
+ if (response[0] != 0) {
+ String message = new String(response, 1, response.length - 1, StandardCharsets.UTF_8);
+ throw new Bls12381WasmException("BLS12-381 WASM error from " + exportName + ": " + message);
+ }
+ return Arrays.copyOfRange(response, 1, response.length);
+ }
+
+ private static byte[] concat(byte[] left, byte[] right) {
+ byte[] out = Arrays.copyOf(left, left.length + right.length);
+ System.arraycopy(right, 0, out, left.length, right.length);
+ return out;
+ }
+
+}
diff --git a/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmException.java b/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmException.java
new file mode 100644
index 0000000..dd32ed7
--- /dev/null
+++ b/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmException.java
@@ -0,0 +1,14 @@
+package com.bloxbean.cardano.zeroj.bls12381.wasm;
+
+/**
+ * Runtime exception raised by the Chicory-backed BLS12-381 provider.
+ */
+public class Bls12381WasmException extends RuntimeException {
+ public Bls12381WasmException(String message) {
+ super(message);
+ }
+
+ public Bls12381WasmException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/WasmBls12381Provider.java b/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/WasmBls12381Provider.java
new file mode 100644
index 0000000..59558be
--- /dev/null
+++ b/zeroj-bls12381-wasm/src/main/java/com/bloxbean/cardano/zeroj/bls12381/wasm/WasmBls12381Provider.java
@@ -0,0 +1,63 @@
+package com.bloxbean.cardano.zeroj.bls12381.wasm;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * BLS12-381 provider backed by zkcrypto/bls12_381 compiled to WASM and executed by Chicory.
+ */
+public final class WasmBls12381Provider implements Bls12381Provider {
+ private final Bls12381WasmClient client;
+
+ public WasmBls12381Provider(Bls12381WasmClient client) {
+ this.client = Objects.requireNonNull(client, "client required");
+ }
+
+ public static WasmBls12381Provider createDefault() {
+ return new WasmBls12381Provider(Bls12381WasmClient.createDefault());
+ }
+
+ @Override
+ public String id() {
+ return "zeroj-bls12381-wasm-zkcrypto";
+ }
+
+ @Override
+ public G1Point g1Generator() {
+ return client.g1Generator();
+ }
+
+ @Override
+ public G2Point g2Generator() {
+ return client.g2Generator();
+ }
+
+ @Override
+ public G1Point g1ScalarMul(G1Point point, BigInteger scalar) {
+ return client.g1ScalarMul(point, scalar);
+ }
+
+ @Override
+ public G2Point g2ScalarMul(G2Point point, BigInteger scalar) {
+ return client.g2ScalarMul(point, scalar);
+ }
+
+ @Override
+ public G1Point g1SecretScalarMul(G1Point point, BigInteger scalar) {
+ return client.g1ScalarMul(point, scalar);
+ }
+
+ @Override
+ public G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) {
+ return client.g2ScalarMul(point, scalar);
+ }
+
+ @Override
+ public boolean pairingProductIsIdentity(G1Point[] g1Points, G2Point[] g2Points) {
+ return client.pairingProductIsIdentity(g1Points, g2Points);
+ }
+}
diff --git a/zeroj-bls12381-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bls12381-wasm/resource-config.json b/zeroj-bls12381-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bls12381-wasm/resource-config.json
new file mode 100644
index 0000000..8d47fe2
--- /dev/null
+++ b/zeroj-bls12381-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bls12381-wasm/resource-config.json
@@ -0,0 +1,9 @@
+{
+ "resources": {
+ "includes": [
+ {
+ "pattern": "\\Qzeroj-bls12381/zeroj_bls12381.wasm\\E"
+ }
+ ]
+ }
+}
diff --git a/zeroj-bls12381-wasm/src/test/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClientTest.java b/zeroj-bls12381-wasm/src/test/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClientTest.java
new file mode 100644
index 0000000..16b2e4e
--- /dev/null
+++ b/zeroj-bls12381-wasm/src/test/java/com/bloxbean/cardano/zeroj/bls12381/wasm/Bls12381WasmClientTest.java
@@ -0,0 +1,274 @@
+package com.bloxbean.cardano.zeroj.bls12381.wasm;
+
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+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.math.BigInteger;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class Bls12381WasmClientTest {
+
+ private final Bls12381WasmClient client = Bls12381WasmClient.createDefault();
+
+ @Test
+ void wasmModule_hasNoHostImportsAndExpectedExports() throws IOException {
+ var module = Parser.parse(loadDefaultWasm());
+ assertEquals(0, module.importSection().importCount());
+
+ 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_bls12381_version"));
+ assertTrue(exports.contains("zeroj_bls12381_g1_generator"));
+ assertTrue(exports.contains("zeroj_bls12381_g2_generator"));
+ assertTrue(exports.contains("zeroj_bls12381_g1_scalar_mul"));
+ assertTrue(exports.contains("zeroj_bls12381_g2_scalar_mul"));
+ assertTrue(exports.contains("zeroj_bls12381_pairing_check"));
+ assertTrue(exports.contains("alloc"));
+ assertTrue(exports.contains("dealloc"));
+ }
+
+ @Test
+ void generators_matchPureJavaConstants() {
+ assertEquals(Bls12381Generators.G1, client.g1Generator());
+ assertEquals(Bls12381Generators.G2, client.g2Generator());
+ }
+
+ @Test
+ void scalarMul_matchesPureJavaProvider() {
+ var pure = Bls12381Providers.pureJava();
+ BigInteger scalar = BigInteger.valueOf(42);
+
+ assertEquals(pure.g1ScalarMul(Bls12381Generators.G1, scalar),
+ client.g1ScalarMul(Bls12381Generators.G1, scalar));
+ assertEquals(pure.g2ScalarMul(Bls12381Generators.G2, scalar),
+ client.g2ScalarMul(Bls12381Generators.G2, scalar));
+ }
+
+ @Test
+ void scalarMul_byZero_returnsInfinity() {
+ assertEquals(G1Point.INFINITY, client.g1ScalarMul(Bls12381Generators.G1, BigInteger.ZERO));
+ assertEquals(G2Point.INFINITY, client.g2ScalarMul(Bls12381Generators.G2, BigInteger.ZERO));
+ }
+
+ @Test
+ void scalarMul_reducesScalarsLikePureJavaProvider() {
+ var pure = Bls12381Providers.pureJava();
+ var r = Bls12381Generators.SCALAR_FIELD_ORDER;
+
+ for (BigInteger scalar : new BigInteger[]{r, r.add(BigInteger.ONE), BigInteger.valueOf(-1)}) {
+ assertEquals(pure.g1ScalarMul(Bls12381Generators.G1, scalar),
+ client.g1ScalarMul(Bls12381Generators.G1, scalar));
+ assertEquals(pure.g2ScalarMul(Bls12381Generators.G2, scalar),
+ client.g2ScalarMul(Bls12381Generators.G2, scalar));
+ }
+ }
+
+ @Test
+ void scalarMul_rejectsInvalidPointsBeforeWasmCall() {
+ assertThrows(IllegalArgumentException.class,
+ () -> client.g1ScalarMul(new G1Point(Fp.ZERO, Fp.ZERO), BigInteger.ONE));
+ assertThrows(IllegalArgumentException.class,
+ () -> client.g2ScalarMul(new G2Point(Fp2.ZERO, Fp2.ZERO), BigInteger.ONE));
+ }
+
+ @Test
+ void rawWasmInvocation_reportsWrongLengthAndInvalidPointErrors() {
+ assertThrows(Bls12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bls12381_g1_scalar_mul", new byte[1]));
+ assertThrows(Bls12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bls12381_pairing_check", new byte[3]));
+
+ byte[] invalidG1ScalarMul = new byte[128];
+ assertThrows(Bls12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bls12381_g1_scalar_mul", invalidG1ScalarMul));
+ }
+
+ @Test
+ void rawWasmInvocation_repeatedErrorsDoNotPoisonClient() {
+ for (int i = 0; i < 100; i++) {
+ assertThrows(Bls12381WasmException.class,
+ () -> client.invokeRawForTesting("zeroj_bls12381_g2_scalar_mul", new byte[7]));
+ }
+ assertEquals(Bls12381Generators.G1, client.g1Generator());
+ }
+
+ @Test
+ void malformedResponseLengthStillFreesResponseAllocation() {
+ var malformed = new Bls12381WasmClient(malformedResponseWasm());
+
+ assertThrows(Bls12381WasmException.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 malformedNoArgResponseLengthStillFreesResponseAllocation() {
+ var malformed = new Bls12381WasmClient(malformedResponseWasm());
+
+ assertThrows(Bls12381WasmException.class,
+ () -> malformed.invokeNoArgRawForTesting("malformed_noarg"));
+
+ assertEquals(1, malformed.invokeExportForTesting("dealloc_count"));
+ assertEquals(4, malformed.invokeExportForTesting("last_dealloc_len"));
+ }
+
+ @Test
+ void pairingProduct_matchesExpectedIdentityResult() {
+ G1Point g1 = Bls12381Generators.G1;
+ G2Point g2 = Bls12381Generators.G2;
+
+ assertFalse(client.pairingProductIsIdentity(new G1Point[]{g1}, new G2Point[]{g2}));
+ assertTrue(client.pairingProductIsIdentity(new G1Point[]{g1, g1.negate()}, new G2Point[]{g2, g2}));
+ }
+
+ @Test
+ void provider_implementsSharedSpi() {
+ var provider = WasmBls12381Provider.createDefault();
+
+ assertEquals("zeroj-bls12381-wasm-zkcrypto", provider.id());
+ assertEquals(Bls12381Generators.G1, provider.g1Generator());
+ assertEquals(Bls12381Generators.G1,
+ provider.g1FromCompressed(provider.g1ToCompressed(Bls12381Generators.G1)));
+ assertEquals(provider.g1ScalarMulGenerator(BigInteger.valueOf(42)),
+ provider.g1SecretScalarMulGenerator(BigInteger.valueOf(42)));
+ assertEquals(provider.g2ScalarMulGenerator(BigInteger.valueOf(42)),
+ provider.g2SecretScalarMulGenerator(BigInteger.valueOf(42)));
+ assertTrue(provider.g1HashToCurve(
+ "abc".getBytes(java.nio.charset.StandardCharsets.US_ASCII),
+ "QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_".getBytes(java.nio.charset.StandardCharsets.US_ASCII)).isValid());
+ assertTrue(provider.pairingProductIsIdentity(
+ new G1Point[]{Bls12381Generators.G1, Bls12381Generators.G1.negate()},
+ new G2Point[]{Bls12381Generators.G2, Bls12381Generators.G2}));
+ }
+
+ private static byte[] loadDefaultWasm() throws IOException {
+ try (var in = Bls12381WasmClient.class.getResourceAsStream(Bls12381WasmClient.DEFAULT_RESOURCE)) {
+ assertNotNull(in);
+ return in.readAllBytes();
+ }
+ }
+
+ 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_bls12381_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);
+ code(out, 0x00, 0x41, 0x01, 0x0b);
+ code(out, 0x00, 0x41, 0x80, 0x08, 0x0b);
+ code(out,
+ 0x00,
+ 0x41, 0x00,
+ 0x41, 0x00,
+ 0x28, 0x02, 0x00,
+ 0x41, 0x01,
+ 0x6a,
+ 0x36, 0x02, 0x00,
+ 0x41, 0x04,
+ 0x20, 0x01,
+ 0x36, 0x02, 0x00,
+ 0x0b);
+ code(out, 0x00, 0x41, 0x08, 0x41, 0x00, 0x36, 0x02, 0x00, 0x41, 0x08, 0x0b);
+ code(out, 0x00, 0x41, 0x08, 0x41, 0x00, 0x36, 0x02, 0x00, 0x41, 0x08, 0x0b);
+ code(out, 0x00, 0x41, 0x00, 0x28, 0x02, 0x00, 0x0b);
+ code(out, 0x00, 0x41, 0x04, 0x28, 0x02, 0x00, 0x0b);
+ });
+ 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 interface SectionWriter {
+ void write(ByteArrayOutputStream out);
+ }
+}
diff --git a/zeroj-bls12381/build.gradle b/zeroj-bls12381/build.gradle
new file mode 100644
index 0000000..e61c99f
--- /dev/null
+++ b/zeroj-bls12381/build.gradle
@@ -0,0 +1,19 @@
+plugins {
+ id 'java-library'
+}
+
+description = 'ZeroJ shared pure Java BLS12-381 primitives'
+
+dependencies {
+}
+
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ pom {
+ name = 'ZeroJ BLS12-381'
+ description = 'Shared pure Java BLS12-381 field, curve, pairing, and provider primitives'
+ }
+ }
+ }
+}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Codecs.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Codecs.java
new file mode 100644
index 0000000..40874c4
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Codecs.java
@@ -0,0 +1,293 @@
+package com.bloxbean.cardano.zeroj.bls12381;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * BLS12-381 byte encodings used by ZeroJ providers.
+ *
+ * G1 uncompressed encoding is {@code x || y}. G2 uncompressed encoding is
+ * {@code x.c1 || x.c0 || y.c1 || y.c0}, matching the standard BLS12-381
+ * serialization used by zkcrypto/blst.
+ */
+public final class Bls12381Codecs {
+ public static final int FP_BYTES = 48;
+ public static final int SCALAR_BYTES = 32;
+ public static final int G1_COMPRESSED_BYTES = FP_BYTES;
+ public static final int G2_COMPRESSED_BYTES = FP_BYTES * 2;
+ public static final int G1_UNCOMPRESSED_BYTES = FP_BYTES * 2;
+ public static final int G2_UNCOMPRESSED_BYTES = FP_BYTES * 4;
+
+ private static final int COMPRESSED_FLAG = 0x80;
+ private static final int INFINITY_FLAG = 0x40;
+ private static final int SORT_FLAG = 0x20;
+ private static final int FLAG_MASK = 0xe0;
+
+ private Bls12381Codecs() {}
+
+ public static byte[] scalarToLittleEndian32(BigInteger scalar) {
+ Objects.requireNonNull(scalar, "scalar required");
+ if (scalar.signum() < 0 || scalar.compareTo(Bls12381Generators.SCALAR_FIELD_ORDER) >= 0) {
+ throw new IllegalArgumentException("Scalar is outside BLS12-381 Fr");
+ }
+ byte[] be = fixedBigEndian(scalar, SCALAR_BYTES);
+ reverse(be);
+ return be;
+ }
+
+ public static byte[] scalarToLittleEndian32Reduced(BigInteger scalar) {
+ Objects.requireNonNull(scalar, "scalar required");
+ return scalarToLittleEndian32(scalar.mod(Bls12381Generators.SCALAR_FIELD_ORDER));
+ }
+
+ public static byte[] g1ToUncompressed(G1Point point) {
+ Objects.requireNonNull(point, "point required");
+ byte[] out = new byte[G1_UNCOMPRESSED_BYTES];
+ if (point.isInfinity()) {
+ out[0] = INFINITY_FLAG;
+ return out;
+ }
+ writeFp(out, 0, point.x().value());
+ writeFp(out, FP_BYTES, point.y().value());
+ return out;
+ }
+
+ public static byte[] g1ToCompressed(G1Point point) {
+ Objects.requireNonNull(point, "point required");
+ byte[] out = new byte[G1_COMPRESSED_BYTES];
+ if (point.isInfinity()) {
+ out[0] = (byte) (COMPRESSED_FLAG | INFINITY_FLAG);
+ return out;
+ }
+ writeFp(out, 0, point.x().value());
+ out[0] |= (byte) COMPRESSED_FLAG;
+ if (point.y().lexicographicallyLargest()) {
+ out[0] |= SORT_FLAG;
+ }
+ return out;
+ }
+
+ public static G1Point g1FromUncompressed(byte[] bytes) {
+ return requireValid(g1FromUncompressedUnchecked(bytes));
+ }
+
+ public static G1Point g1FromCompressed(byte[] bytes) {
+ return requireValid(g1FromCompressedUnchecked(bytes));
+ }
+
+ public static G1Point g1FromCompressedUnchecked(byte[] bytes) {
+ requireLength(bytes, G1_COMPRESSED_BYTES, "G1 compressed");
+ byte[] copy = bytes.clone();
+ boolean compressed = (copy[0] & COMPRESSED_FLAG) != 0;
+ boolean infinity = (copy[0] & INFINITY_FLAG) != 0;
+ boolean sorted = (copy[0] & SORT_FLAG) != 0;
+ copy[0] &= ~FLAG_MASK;
+ if (!compressed) {
+ throw new IllegalArgumentException("Invalid G1 compressed flags");
+ }
+ if (infinity) {
+ if (sorted || !allZero(copy)) {
+ throw new IllegalArgumentException("Invalid G1 compressed infinity encoding");
+ }
+ return G1Point.INFINITY;
+ }
+
+ var x = Fp.of(readFp(copy, 0));
+ var y = x.square().mul(x).add(Fp.of(4)).sqrt()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid G1 compressed point"));
+ if (y.lexicographicallyLargest() != sorted) {
+ y = y.neg();
+ }
+ return new G1Point(x, y);
+ }
+
+ public static G1Point g1FromUncompressedUnchecked(byte[] bytes) {
+ requireLength(bytes, G1_UNCOMPRESSED_BYTES, "G1 uncompressed");
+ byte[] copy = bytes.clone();
+ boolean compressed = (copy[0] & COMPRESSED_FLAG) != 0;
+ boolean infinity = (copy[0] & INFINITY_FLAG) != 0;
+ boolean sorted = (copy[0] & SORT_FLAG) != 0;
+ copy[0] &= ~FLAG_MASK;
+ if (compressed || sorted) {
+ throw new IllegalArgumentException("Invalid G1 uncompressed flags");
+ }
+ if (infinity) {
+ if (!allZero(copy)) {
+ throw new IllegalArgumentException("Invalid G1 infinity encoding");
+ }
+ return G1Point.INFINITY;
+ }
+ return new G1Point(
+ Fp.of(readFp(copy, 0)),
+ Fp.of(readFp(copy, FP_BYTES)));
+ }
+
+ public static byte[] g2ToUncompressed(G2Point point) {
+ Objects.requireNonNull(point, "point required");
+ byte[] out = new byte[G2_UNCOMPRESSED_BYTES];
+ if (point.isInfinity()) {
+ out[0] = INFINITY_FLAG;
+ return out;
+ }
+ writeFp(out, 0, point.x().c1().value());
+ writeFp(out, FP_BYTES, point.x().c0().value());
+ writeFp(out, FP_BYTES * 2, point.y().c1().value());
+ writeFp(out, FP_BYTES * 3, point.y().c0().value());
+ return out;
+ }
+
+ public static byte[] g2ToCompressed(G2Point point) {
+ Objects.requireNonNull(point, "point required");
+ byte[] out = new byte[G2_COMPRESSED_BYTES];
+ if (point.isInfinity()) {
+ out[0] = (byte) (COMPRESSED_FLAG | INFINITY_FLAG);
+ return out;
+ }
+ writeFp(out, 0, point.x().c1().value());
+ writeFp(out, FP_BYTES, point.x().c0().value());
+ out[0] |= (byte) COMPRESSED_FLAG;
+ if (point.y().lexicographicallyLargest()) {
+ out[0] |= SORT_FLAG;
+ }
+ return out;
+ }
+
+ public static G2Point g2FromUncompressed(byte[] bytes) {
+ return requireValid(g2FromUncompressedUnchecked(bytes));
+ }
+
+ public static G2Point g2FromCompressed(byte[] bytes) {
+ return requireValid(g2FromCompressedUnchecked(bytes));
+ }
+
+ public static G2Point g2FromCompressedUnchecked(byte[] bytes) {
+ requireLength(bytes, G2_COMPRESSED_BYTES, "G2 compressed");
+ byte[] copy = bytes.clone();
+ boolean compressed = (copy[0] & COMPRESSED_FLAG) != 0;
+ boolean infinity = (copy[0] & INFINITY_FLAG) != 0;
+ boolean sorted = (copy[0] & SORT_FLAG) != 0;
+ copy[0] &= ~FLAG_MASK;
+ if (!compressed) {
+ throw new IllegalArgumentException("Invalid G2 compressed flags");
+ }
+ if (infinity) {
+ if (sorted || !allZero(copy)) {
+ throw new IllegalArgumentException("Invalid G2 compressed infinity encoding");
+ }
+ return G2Point.INFINITY;
+ }
+
+ var xc1 = Fp.of(readFp(copy, 0));
+ var xc0 = Fp.of(readFp(copy, FP_BYTES));
+ var x = Fp2.of(xc0, xc1);
+ var twistB = Fp2.of(Fp.of(4), Fp.of(4));
+ var y = x.square().mul(x).add(twistB).sqrt()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid G2 compressed point"));
+ if (y.lexicographicallyLargest() != sorted) {
+ y = y.neg();
+ }
+ return new G2Point(x, y);
+ }
+
+ public static G2Point g2FromUncompressedUnchecked(byte[] bytes) {
+ requireLength(bytes, G2_UNCOMPRESSED_BYTES, "G2 uncompressed");
+ byte[] copy = bytes.clone();
+ boolean compressed = (copy[0] & COMPRESSED_FLAG) != 0;
+ boolean infinity = (copy[0] & INFINITY_FLAG) != 0;
+ boolean sorted = (copy[0] & SORT_FLAG) != 0;
+ copy[0] &= ~FLAG_MASK;
+ if (compressed || sorted) {
+ throw new IllegalArgumentException("Invalid G2 uncompressed flags");
+ }
+ if (infinity) {
+ if (!allZero(copy)) {
+ throw new IllegalArgumentException("Invalid G2 infinity encoding");
+ }
+ return G2Point.INFINITY;
+ }
+ var xc1 = Fp.of(readFp(copy, 0));
+ var xc0 = Fp.of(readFp(copy, FP_BYTES));
+ var yc1 = Fp.of(readFp(copy, FP_BYTES * 2));
+ var yc0 = Fp.of(readFp(copy, FP_BYTES * 3));
+ return new G2Point(Fp2.of(xc0, xc1), Fp2.of(yc0, yc1));
+ }
+
+ public static G1Point requireValid(G1Point point) {
+ Objects.requireNonNull(point, "G1 point required");
+ if (!point.isOnCurve()) {
+ throw new IllegalArgumentException("G1 point is not on BLS12-381");
+ }
+ if (!point.isInSubgroup()) {
+ throw new IllegalArgumentException("G1 point is not in the prime-order subgroup");
+ }
+ return point;
+ }
+
+ public static G2Point requireValid(G2Point point) {
+ Objects.requireNonNull(point, "G2 point required");
+ if (!point.isOnCurve()) {
+ throw new IllegalArgumentException("G2 point is not on BLS12-381 twist");
+ }
+ if (!point.isInSubgroup()) {
+ throw new IllegalArgumentException("G2 point is not in the prime-order subgroup");
+ }
+ return point;
+ }
+
+ private static void writeFp(byte[] buf, int offset, BigInteger value) {
+ byte[] bytes = fixedBigEndian(value, FP_BYTES);
+ System.arraycopy(bytes, 0, buf, offset, FP_BYTES);
+ }
+
+ private static BigInteger readFp(byte[] bytes, int offset) {
+ BigInteger value = new BigInteger(1, Arrays.copyOfRange(bytes, offset, offset + FP_BYTES));
+ if (value.compareTo(Fp.P) >= 0) {
+ throw new IllegalArgumentException("Field element is outside BLS12-381 Fp");
+ }
+ return value;
+ }
+
+ private static byte[] fixedBigEndian(BigInteger value, int length) {
+ if (value.signum() < 0) {
+ throw new IllegalArgumentException("Value must be non-negative");
+ }
+ 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;
+ }
+
+ 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 boolean allZero(byte[] bytes) {
+ int acc = 0;
+ for (byte b : bytes) {
+ acc |= b & 0xff;
+ }
+ return acc == 0;
+ }
+
+ private static void reverse(byte[] bytes) {
+ for (int i = 0, j = bytes.length - 1; i < j; i++, j--) {
+ byte tmp = bytes[i];
+ bytes[i] = bytes[j];
+ bytes[j] = tmp;
+ }
+ }
+}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Generators.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Generators.java
new file mode 100644
index 0000000..ce41cb2
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Generators.java
@@ -0,0 +1,29 @@
+package com.bloxbean.cardano.zeroj.bls12381;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+
+import java.math.BigInteger;
+
+/**
+ * Standard BLS12-381 generator points and scalar-field constants.
+ */
+public final class Bls12381Generators {
+ private Bls12381Generators() {}
+
+ public static final BigInteger SCALAR_FIELD_ORDER = G1Point.R;
+
+ public static final G1Point G1 = new G1Point(
+ Fp.of(new BigInteger("17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb", 16)),
+ Fp.of(new BigInteger("08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1", 16)));
+
+ public static final G2Point G2 = new G2Point(
+ Fp2.of(
+ Fp.of(new BigInteger("024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8", 16)),
+ Fp.of(new BigInteger("13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e", 16))),
+ Fp2.of(
+ Fp.of(new BigInteger("0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801", 16)),
+ Fp.of(new BigInteger("0606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be", 16))));
+}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Hash.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Hash.java
new file mode 100644
index 0000000..5d93554
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381Hash.java
@@ -0,0 +1,284 @@
+package com.bloxbean.cardano.zeroj.bls12381;
+
+import java.math.BigInteger;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Hash helpers shared by BLS12-381 providers.
+ */
+public final class Bls12381Hash {
+ private static final int SHA256_BYTES = 32;
+ private static final int SHA256_BLOCK_BYTES = 64;
+ private static final int SHAKE256_RATE_BYTES = 136;
+ private static final int SHAKE256_OVERSIZE_DST_BYTES = 32;
+ private static final int SCALAR_HASH_BYTES = 48;
+ private static final byte[] OVERSIZE_DST_PREFIX = "H2C-OVERSIZE-DST-".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+ private static final int[] KECCAK_ROTATION = {
+ 0, 1, 62, 28, 27,
+ 36, 44, 6, 55, 20,
+ 3, 10, 43, 25, 39,
+ 41, 45, 15, 21, 8,
+ 18, 2, 61, 56, 14
+ };
+ private static final long[] KECCAK_ROUND_CONSTANTS = {
+ 0x0000000000000001L, 0x0000000000008082L, 0x800000000000808aL, 0x8000000080008000L,
+ 0x000000000000808bL, 0x0000000080000001L, 0x8000000080008081L, 0x8000000000008009L,
+ 0x000000000000008aL, 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000aL,
+ 0x000000008000808bL, 0x800000000000008bL, 0x8000000000008089L, 0x8000000000008003L,
+ 0x8000000000008002L, 0x8000000000000080L, 0x000000000000800aL, 0x800000008000000aL,
+ 0x8000000080008081L, 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L
+ };
+
+ private Bls12381Hash() {}
+
+ public static BigInteger hashToScalar(byte[] message, byte[] dst) {
+ byte[] uniform = expandMessageXmdSha256(message, dst, SCALAR_HASH_BYTES);
+ return new BigInteger(1, uniform).mod(Bls12381Generators.SCALAR_FIELD_ORDER);
+ }
+
+ public static BigInteger hashToScalarXofShake256(byte[] message, byte[] dst) {
+ byte[] uniform = expandMessageXofShake256(message, dst, SCALAR_HASH_BYTES);
+ return new BigInteger(1, uniform).mod(Bls12381Generators.SCALAR_FIELD_ORDER);
+ }
+
+ public static G1Point hashToG1(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.hashToG1(message, dst);
+ }
+
+ public static G1Point encodeToG1(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.encodeToG1(message, dst);
+ }
+
+ public static G1Point hashToG1XofShake256(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.hashToG1XofShake256(message, dst);
+ }
+
+ public static G1Point encodeToG1XofShake256(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.encodeToG1XofShake256(message, dst);
+ }
+
+ public static G2Point hashToG2(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.hashToG2(message, dst);
+ }
+
+ public static G2Point encodeToG2(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.encodeToG2(message, dst);
+ }
+
+ public static G2Point hashToG2XofShake256(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.hashToG2XofShake256(message, dst);
+ }
+
+ public static G2Point encodeToG2XofShake256(byte[] message, byte[] dst) {
+ return Bls12381HashToCurve.encodeToG2XofShake256(message, dst);
+ }
+
+ public static byte[] expandMessageXmdSha256(byte[] message, byte[] dst, int lenInBytes) {
+ Objects.requireNonNull(message, "message required");
+ Objects.requireNonNull(dst, "dst required");
+ if (lenInBytes < 0 || lenInBytes > 255 * SHA256_BYTES) {
+ throw new IllegalArgumentException("Invalid expand_message_xmd length: " + lenInBytes);
+ }
+ if (lenInBytes == 0) {
+ return new byte[0];
+ }
+ byte[] effectiveDst = dst;
+ if (effectiveDst.length > 255) {
+ effectiveDst = sha256(concat(OVERSIZE_DST_PREFIX, effectiveDst));
+ }
+
+ int ell = (lenInBytes + SHA256_BYTES - 1) / SHA256_BYTES;
+ byte[] dstPrime = concat(effectiveDst, new byte[]{(byte) effectiveDst.length});
+ byte[] zPad = new byte[SHA256_BLOCK_BYTES];
+ byte[] lenBytes = new byte[]{(byte) (lenInBytes >>> 8), (byte) lenInBytes};
+
+ byte[] b0 = sha256(concat(zPad, message, lenBytes, new byte[]{0}, dstPrime));
+ byte[] bi = sha256(concat(b0, new byte[]{1}, dstPrime));
+ byte[] uniform = new byte[ell * SHA256_BYTES];
+ System.arraycopy(bi, 0, uniform, 0, SHA256_BYTES);
+
+ for (int i = 2; i <= ell; i++) {
+ bi = sha256(concat(xor(b0, bi), new byte[]{(byte) i}, dstPrime));
+ System.arraycopy(bi, 0, uniform, (i - 1) * SHA256_BYTES, SHA256_BYTES);
+ }
+ return Arrays.copyOf(uniform, lenInBytes);
+ }
+
+ public static byte[] expandMessageXofShake256(byte[] message, byte[] dst, int lenInBytes) {
+ Objects.requireNonNull(message, "message required");
+ Objects.requireNonNull(dst, "dst required");
+ if (lenInBytes < 0 || lenInBytes > 0xffff) {
+ throw new IllegalArgumentException("Invalid expand_message_xof length: " + lenInBytes);
+ }
+ if (lenInBytes == 0) {
+ return new byte[0];
+ }
+ byte[] effectiveDst = dst;
+ if (effectiveDst.length > 255) {
+ effectiveDst = shake256(concat(OVERSIZE_DST_PREFIX, effectiveDst), SHAKE256_OVERSIZE_DST_BYTES);
+ }
+
+ byte[] dstPrime = concat(effectiveDst, new byte[]{(byte) effectiveDst.length});
+ byte[] lenBytes = new byte[]{(byte) (lenInBytes >>> 8), (byte) lenInBytes};
+ return shake256(concat(message, lenBytes, dstPrime), lenInBytes);
+ }
+
+ static BigInteger[] hashToFp(byte[] message, byte[] dst, int count) {
+ byte[] uniform = expandMessageXmdSha256(message, dst, count * 64);
+ BigInteger[] out = new BigInteger[count];
+ for (int i = 0; i < count; i++) {
+ out[i] = new BigInteger(1, Arrays.copyOfRange(uniform, i * 64, (i + 1) * 64))
+ .mod(com.bloxbean.cardano.zeroj.bls12381.field.Fp.P);
+ }
+ return out;
+ }
+
+ static BigInteger[] hashToFpXofShake256(byte[] message, byte[] dst, int count) {
+ byte[] uniform = expandMessageXofShake256(message, dst, count * 64);
+ BigInteger[] out = new BigInteger[count];
+ for (int i = 0; i < count; i++) {
+ out[i] = new BigInteger(1, Arrays.copyOfRange(uniform, i * 64, (i + 1) * 64))
+ .mod(com.bloxbean.cardano.zeroj.bls12381.field.Fp.P);
+ }
+ return out;
+ }
+
+ static BigInteger[][] hashToFp2(byte[] message, byte[] dst, int count) {
+ byte[] uniform = expandMessageXmdSha256(message, dst, count * 2 * 64);
+ BigInteger[][] out = new BigInteger[count][2];
+ for (int i = 0; i < count; i++) {
+ int offset = i * 128;
+ out[i][0] = new BigInteger(1, Arrays.copyOfRange(uniform, offset, offset + 64))
+ .mod(com.bloxbean.cardano.zeroj.bls12381.field.Fp.P);
+ out[i][1] = new BigInteger(1, Arrays.copyOfRange(uniform, offset + 64, offset + 128))
+ .mod(com.bloxbean.cardano.zeroj.bls12381.field.Fp.P);
+ }
+ return out;
+ }
+
+ static BigInteger[][] hashToFp2XofShake256(byte[] message, byte[] dst, int count) {
+ byte[] uniform = expandMessageXofShake256(message, dst, count * 2 * 64);
+ BigInteger[][] out = new BigInteger[count][2];
+ for (int i = 0; i < count; i++) {
+ int offset = i * 128;
+ out[i][0] = new BigInteger(1, Arrays.copyOfRange(uniform, offset, offset + 64))
+ .mod(com.bloxbean.cardano.zeroj.bls12381.field.Fp.P);
+ out[i][1] = new BigInteger(1, Arrays.copyOfRange(uniform, offset + 64, offset + 128))
+ .mod(com.bloxbean.cardano.zeroj.bls12381.field.Fp.P);
+ }
+ return out;
+ }
+
+ private static byte[] sha256(byte[] input) {
+ try {
+ return MessageDigest.getInstance("SHA-256").digest(input);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("SHA-256 is not available", e);
+ }
+ }
+
+ private static byte[] shake256(byte[] input, int lenInBytes) {
+ if (lenInBytes < 0) {
+ throw new IllegalArgumentException("Invalid SHAKE256 output length: " + lenInBytes);
+ }
+ long[] state = new long[25];
+ int offset = 0;
+ while (input.length - offset >= SHAKE256_RATE_BYTES) {
+ absorbBlock(state, input, offset);
+ keccakF1600(state);
+ offset += SHAKE256_RATE_BYTES;
+ }
+
+ byte[] finalBlock = new byte[SHAKE256_RATE_BYTES];
+ int remaining = input.length - offset;
+ System.arraycopy(input, offset, finalBlock, 0, remaining);
+ finalBlock[remaining] ^= 0x1f;
+ finalBlock[SHAKE256_RATE_BYTES - 1] ^= (byte) 0x80;
+ absorbBlock(state, finalBlock, 0);
+ keccakF1600(state);
+
+ byte[] out = new byte[lenInBytes];
+ int outOffset = 0;
+ while (outOffset < lenInBytes) {
+ int blockLen = Math.min(SHAKE256_RATE_BYTES, lenInBytes - outOffset);
+ squeezeBlock(state, out, outOffset, blockLen);
+ outOffset += blockLen;
+ if (outOffset < lenInBytes) {
+ keccakF1600(state);
+ }
+ }
+ return out;
+ }
+
+ private static void absorbBlock(long[] state, byte[] block, int offset) {
+ for (int i = 0; i < SHAKE256_RATE_BYTES; i++) {
+ state[i >>> 3] ^= (block[offset + i] & 0xffL) << (8 * (i & 7));
+ }
+ }
+
+ private static void squeezeBlock(long[] state, byte[] out, int offset, int len) {
+ for (int i = 0; i < len; i++) {
+ out[offset + i] = (byte) (state[i >>> 3] >>> (8 * (i & 7)));
+ }
+ }
+
+ private static void keccakF1600(long[] state) {
+ long[] b = new long[25];
+ long[] c = new long[5];
+ long[] d = new long[5];
+ for (long roundConstant : KECCAK_ROUND_CONSTANTS) {
+ for (int x = 0; x < 5; x++) {
+ c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
+ }
+ for (int x = 0; x < 5; x++) {
+ d[x] = c[(x + 4) % 5] ^ Long.rotateLeft(c[(x + 1) % 5], 1);
+ }
+ for (int x = 0; x < 5; x++) {
+ for (int y = 0; y < 5; y++) {
+ state[x + 5 * y] ^= d[x];
+ }
+ }
+
+ for (int x = 0; x < 5; x++) {
+ for (int y = 0; y < 5; y++) {
+ b[y + 5 * ((2 * x + 3 * y) % 5)] =
+ Long.rotateLeft(state[x + 5 * y], KECCAK_ROTATION[x + 5 * y]);
+ }
+ }
+ for (int x = 0; x < 5; x++) {
+ for (int y = 0; y < 5; y++) {
+ state[x + 5 * y] = b[x + 5 * y]
+ ^ ((~b[((x + 1) % 5) + 5 * y]) & b[((x + 2) % 5) + 5 * y]);
+ }
+ }
+ state[0] ^= roundConstant;
+ }
+ }
+
+ private static byte[] xor(byte[] left, byte[] right) {
+ byte[] out = new byte[left.length];
+ for (int i = 0; i < left.length; i++) {
+ out[i] = (byte) (left[i] ^ right[i]);
+ }
+ return out;
+ }
+
+ private 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;
+ }
+}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurve.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurve.java
new file mode 100644
index 0000000..592a5ec
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurve.java
@@ -0,0 +1,306 @@
+package com.bloxbean.cardano.zeroj.bls12381;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * RFC 9380 BLS12-381 hash-to-curve support.
+ */
+final class Bls12381HashToCurve {
+ private static final BigInteger G1_H_EFF = new BigInteger("d201000000010001", 16);
+ private static final BigInteger G2_H_EFF = new BigInteger(
+ "bc69f08f2ee75b3584c6a0ea91b352888e2a8e9145ad7689986ff031508ffe1329c2f178731db956d82bf015d1212b02ec0ec69d7477c1ae954cbc06689f6a359894c0adebbf6b4e8020005aaa95551", 16);
+
+ private static final Fp G1_Z = Fp.of(11);
+ private static final Fp G1_A = fp("144698a3b8e9433d693a02c96d4982b0ea985383ee66a8d8e8981aefd881ac98936f8da0e0f97f5cf428082d584c1d");
+ private static final Fp G1_B = fp("12e2908d11688030018b12e8753eee3b2016c1f0f24f4070a0b9c14fcef35ef55a23215a316ceaa5d1cc48e98e172be0");
+
+ private static final Fp2 G2_Z = Fp2.of(Fp.of(BigInteger.valueOf(-2)), Fp.of(BigInteger.valueOf(-1)));
+ private static final Fp2 G2_A = Fp2.of(Fp.ZERO, Fp.of(240));
+ private static final Fp2 G2_B = Fp2.of(Fp.of(1012), Fp.of(1012));
+
+ private static final Fp[] G1_X_NUM = fpArray(
+ "11a05f2b1e833340b809101dd99815856b303e88a2d7005ff2627b56cdb4e2c85610c2d5f2e62d6eaeac1662734649b7",
+ "17294ed3e943ab2f0588bab22147a81c7c17e75b2f6a8417f565e33c70d1e86b4838f2a6f318c356e834eef1b3cb83bb",
+ "0d54005db97678ec1d1048c5d10a9a1bce032473295983e56878e501ec68e25c958c3e3d2a09729fe0179f9dac9edcb0",
+ "1778e7166fcc6db74e0609d307e55412d7f5e4656a8dbf25f1b33289f1b330835336e25ce3107193c5b388641d9b6861",
+ "0e99726a3199f4436642b4b3e4118e5499db995a1257fb3f086eeb65982fac18985a286f301e77c451154ce9ac8895d9",
+ "1630c3250d7313ff01d1201bf7a74ab5db3cb17dd952799b9ed3ab9097e68f90a0870d2dcae73d19cd13c1c66f652983",
+ "0d6ed6553fe44d296a3726c38ae652bfb11586264f0f8ce19008e218f9c86b2a8da25128c1052ecaddd7f225a139ed84",
+ "17b81e7701abdbe2e8743884d1117e53356de5ab275b4db1a682c62ef0f2753339b7c8f8c8f475af9ccb5618e3f0c88e",
+ "080d3cf1f9a78fc47b90b33563be990dc43b756ce79f5574a2c596c928c5d1de4fa295f296b74e956d71986a8497e317",
+ "169b1f8e1bcfa7c42e0c37515d138f22dd2ecb803a0c5c99676314baf4bb1b7fa3190b2edc0327797f241067be390c9e",
+ "10321da079ce07e272d8ec09d2565b0dfa7dccdde6787f96d50af36003b14866f69b771f8c285decca67df3f1605fb7b",
+ "06e08c248e260e70bd1e962381edee3d31d79d7e22c837bc23c0bf1bc24c6b68c24b1b80b64d391fa9c8ba2e8ba2d229");
+ private static final Fp[] G1_X_DEN = fpArray(
+ "08ca8d548cff19ae18b2e62f4bd3fa6f01d5ef4ba35b48ba9c9588617fc8ac62b558d681be343df8993cf9fa40d21b1c",
+ "12561a5deb559c4348b4711298e536367041e8ca0cf0800c0126c2588c48bf5713daa8846cb026e9e5c8276ec82b3bff",
+ "0b2962fe57a3225e8137e629bff2991f6f89416f5a718cd1fca64e00b11aceacd6a3d0967c94fedcfcc239ba5cb83e19",
+ "03425581a58ae2fec83aafef7c40eb545b08243f16b1655154cca8abc28d6fd04976d5243eecf5c4130de8938dc62cd8",
+ "13a8e162022914a80a6f1d5f43e7a07dffdfc759a12062bb8d6b44e833b306da9bd29ba81f35781d539d395b3532a21e",
+ "0e7355f8e4e667b955390f7f0506c6e9395735e9ce9cad4d0a43bcef24b8982f7400d24bc4228f11c02df9a29f6304a5",
+ "0772caacf16936190f3e0c63e0596721570f5799af53a1894e2e073062aede9cea73b3538f0de06cec2574496ee84a3a",
+ "14a7ac2a9d64a8b230b3f5b074cf01996e7f63c21bca68a81996e1cdf9822c580fa5b9489d11e2d311f7d99bbdcc5a5e",
+ "0a10ecf6ada54f825e920b3dafc7a3cce07f8d1d7161366b74100da67f39883503826692abba43704776ec3a79a1d641",
+ "095fc13ab9e92ad4476d6e3eb3a56680f682b4ee96f7d03776df533978f31c1593174e4b4b7865002d6384d168ecdd0a");
+ private static final Fp[] G1_Y_NUM = fpArray(
+ "090d97c81ba24ee0259d1f094980dcfa11ad138e48a869522b52af6c956543d3cd0c7aee9b3ba3c2be9845719707bb33",
+ "134996a104ee5811d51036d776fb46831223e96c254f383d0f906343eb67ad34d6c56711962fa8bfe097e75a2e41c696",
+ "0cc786baa966e66f4a384c86a3b49942552e2d658a31ce2c344be4b91400da7d26d521628b00523b8dfe240c72de1f6",
+ "01f86376e8981c217898751ad8746757d42aa7b90eeb791c09e4a3ec03251cf9de405aba9ec61deca6355c77b0e5f4cb",
+ "08cc03fdefe0ff135caf4fe2a21529c4195536fbe3ce50b879833fd221351adc2ee7f8dc099040a841b6daecf2e8fedb",
+ "16603fca40634b6a2211e11db8f0a6a074a7d0d4afadb7bd76505c3d3ad5544e203f6326c95a807299b23ab13633a5f0",
+ "04ab0b9bcfac1bbcb2c977d027796b3ce75bb8ca2be184cb5231413c4d634f3747a87ac2460f415ec961f8855fe9d6f2",
+ "0987c8d5333ab86fde9926bd2ca6c674170a05bfe3bdd81ffd038da6c26c842642f64550fedfe935a15e4ca31870fb29",
+ "09fc4018bd96684be88c9e221e4da1bb8f3abd16679dc26c1e8b6e6a1f20cabe69d65201c78607a360370e577bdba587",
+ "0e1bba7a1186bdb5223abde7ada14a23c42a0ca7915af6fe06985e7ed1e4d43b9b3f7055dd4eba6f2bafaaebca731c30",
+ "19713e47937cd1be0dfd0b8f1d43fb93cd2fcbcb6caf493fd1183e416389e61031bf3a5cce3fbafce813711ad011c132",
+ "18b46a908f36f6deb918c143fed2edcc523559b8aaf0c2462e6bfe7f911f643249d9cdf41b44d606ce07c8a4d0074d8e",
+ "0b182cac101b9399d155096004f53f447aa7b12a3426b08ec02710e807b4633f06c851c1919211f20d4c04f00b971ef8",
+ "0245a394ad1eca9b72fc00ae7be315dc757b3b080d4c158013e6632d3c40659cc6cf90ad1c232a6442d9d3f5db980133",
+ "05c129645e44cf1102a159f748c4a3fc5e673d81d7e86568d9ab0f5d396a7ce46ba1049b6579afb7866b1e715475224b",
+ "15e6be4e990f03ce4ea50b3b42df2eb5cb181d8f84965a3957add4fa95af01b2b665027efec01c7704b456be69c8b604");
+ private static final Fp[] G1_Y_DEN = fpArray(
+ "16112c4c3a9c98b252181140fad0eae9601a6de578980be6eec3232b5be72e7a07f3688ef60c206d01479253b03663c1",
+ "1962d75c2381201e1a0cbd6c43c348b885c84ff731c4d59ca4a10356f453e01f78a4260763529e3532f6102c2e49a03d",
+ "058df3306640da276faaae7d6e8eb15778c4855551ae7f310c35a5dd279cd2eca6757cd636f96f891e2538b53dbf67f2",
+ "16b7d288798e5395f20d23bf89edb4d1d115c5dbddbcd30e123da489e726af41727364f2c28297ada8d26d98445f5416",
+ "0be0e079545f43e4b00cc912f8228ddcc6d19c9f0f69bbb0542eda0fc9dec916a20b15dc0fd2ededda39142311a5001d",
+ "08d9e5297186db2d9fb266eaac783182b70152c65550d881c5ecd87b6f0f5a6449f38db9dfa9cce202c6477faaf9b7ac",
+ "166007c08a99db2fc3ba8734ace9824b5eecfdfa8d0cf8ef5dd365bc400a0051d5fa9c01a58b1fb93d1a1399126a775c",
+ "16a3ef08be3ea7ea03bcddfabba6ff6ee5a4375efa1f4fd7feb34fd206357132b920f5b00801dee460ee415a15812ed9",
+ "1866c8ed336c61231a1be54fd1d74cc4f9fb0ce4c6af5920abc5750c4bf39b4852cfe2f7bb9248836b233d9d55535d4a",
+ "167a55cda70a6e1cea820597d94a84903216f763e13d87bb5308592e7ea7d4fbc7385ea3d529b35e346ef48bb8913f55",
+ "04d2f259eea405bd48f010a01ad2911d9c6dd039bb61a6290e591b36e636a5c871a5c29f4f83060400f8b49cba8f6aa8",
+ "0accbb67481d033ff5852c1e48c50c477f94ff8aefce42d28c0f9a88cea7913516f968986f7ebbea9684b529e2561092",
+ "0ad6b9514c767fe3c3613144b45f1496543346d98adf02267d5ceef9a00d9b8693000763e3b90ac11e99b138573345cc",
+ "02660400eb2e4f3b628bdd0d53cd76f2bf565b94e72927c1cb748df27942480e420517bd8714cc80d1fadc1326ed06f7",
+ "0e0fa1d816ddc03e6b24255e0d7819c171c40f65e273b853324efcd6356caa205ca2f570f13497804415473a1d634b8f");
+
+ private static final Fp2[] G2_X_NUM = fp2Array(
+ fp2("5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97d6", "5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97d6"),
+ fp2("0", "11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71a"),
+ fp2("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71e", "8ab05f8bdd54cde190937e76bc3e447cc27c3d6fbd7063fcd104635a790520c0a395554e5c6aaaa9354ffffffffe38d"),
+ fp2("171d6541fa38ccfaed6dea691f5fb614cb14b4e7f4e810aa22d6108f142b85757098e38d0f671c7188e2aaaaaaaa5ed1", "0"));
+ private static final Fp2[] G2_X_DEN = fp2Array(
+ fp2("0", "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa63"),
+ fp2("0c", "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa9f"));
+ private static final Fp2[] G2_Y_NUM = fp2Array(
+ fp2("1530477c7ab4113b59a4c18b076d11930f7da5d4a07f649bf54439d87d27e500fc8c25ebf8c92f6812cfc71c71c6d706", "1530477c7ab4113b59a4c18b076d11930f7da5d4a07f649bf54439d87d27e500fc8c25ebf8c92f6812cfc71c71c6d706"),
+ fp2("0", "5c759507e8e333ebb5b7a9a47d7ed8532c52d39fd3a042a88b58423c50ae15d5c2638e343d9c71c6238aaaaaaaa97be"),
+ fp2("11560bf17baa99bc32126fced787c88f984f87adf7ae0c7f9a208c6b4f20a4181472aaa9cb8d555526a9ffffffffc71c", "8ab05f8bdd54cde190937e76bc3e447cc27c3d6fbd7063fcd104635a790520c0a395554e5c6aaaa9354ffffffffe38f"),
+ fp2("124c9ad43b6cf79bfbf7043de3811ad0761b0f37a1e26286b0e977c69aa274524e79097a56dc4bd9e1b371c71c718b10", "0"));
+ private static final Fp2[] G2_Y_DEN = fp2Array(
+ fp2("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa8fb", "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa8fb"),
+ fp2("0", "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffa9d3"),
+ fp2("12", "1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa99"));
+
+ private Bls12381HashToCurve() {}
+
+ static G1Point hashToG1(byte[] message, byte[] dst) {
+ BigInteger[] u = Bls12381Hash.hashToFp(message, dst, 2);
+ return clearG1(isoMapG1(simpleSwuG1(Fp.of(u[0]))).add(isoMapG1(simpleSwuG1(Fp.of(u[1])))));
+ }
+
+ static G1Point encodeToG1(byte[] message, byte[] dst) {
+ BigInteger[] u = Bls12381Hash.hashToFp(message, dst, 1);
+ return clearG1(isoMapG1(simpleSwuG1(Fp.of(u[0]))));
+ }
+
+ static G1Point hashToG1XofShake256(byte[] message, byte[] dst) {
+ BigInteger[] u = Bls12381Hash.hashToFpXofShake256(message, dst, 2);
+ return clearG1(isoMapG1(simpleSwuG1(Fp.of(u[0]))).add(isoMapG1(simpleSwuG1(Fp.of(u[1])))));
+ }
+
+ static G1Point encodeToG1XofShake256(byte[] message, byte[] dst) {
+ BigInteger[] u = Bls12381Hash.hashToFpXofShake256(message, dst, 1);
+ return clearG1(isoMapG1(simpleSwuG1(Fp.of(u[0]))));
+ }
+
+ static G2Point hashToG2(byte[] message, byte[] dst) {
+ BigInteger[][] u = Bls12381Hash.hashToFp2(message, dst, 2);
+ var u0 = Fp2.of(Fp.of(u[0][0]), Fp.of(u[0][1]));
+ var u1 = Fp2.of(Fp.of(u[1][0]), Fp.of(u[1][1]));
+ return clearG2(isoMapG2(simpleSwuG2(u0)).add(isoMapG2(simpleSwuG2(u1))));
+ }
+
+ static G2Point encodeToG2(byte[] message, byte[] dst) {
+ BigInteger[][] u = Bls12381Hash.hashToFp2(message, dst, 1);
+ var u0 = Fp2.of(Fp.of(u[0][0]), Fp.of(u[0][1]));
+ return clearG2(isoMapG2(simpleSwuG2(u0)));
+ }
+
+ static G2Point hashToG2XofShake256(byte[] message, byte[] dst) {
+ BigInteger[][] u = Bls12381Hash.hashToFp2XofShake256(message, dst, 2);
+ var u0 = Fp2.of(Fp.of(u[0][0]), Fp.of(u[0][1]));
+ var u1 = Fp2.of(Fp.of(u[1][0]), Fp.of(u[1][1]));
+ return clearG2(isoMapG2(simpleSwuG2(u0)).add(isoMapG2(simpleSwuG2(u1))));
+ }
+
+ static G2Point encodeToG2XofShake256(byte[] message, byte[] dst) {
+ BigInteger[][] u = Bls12381Hash.hashToFp2XofShake256(message, dst, 1);
+ var u0 = Fp2.of(Fp.of(u[0][0]), Fp.of(u[0][1]));
+ return clearG2(isoMapG2(simpleSwuG2(u0)));
+ }
+
+ private static G1Point clearG1(G1Point point) {
+ return Bls12381Codecs.requireValid(point.scalarMul(G1_H_EFF));
+ }
+
+ private static G2Point clearG2(G2Point point) {
+ return Bls12381Codecs.requireValid(point.scalarMul(G2_H_EFF));
+ }
+
+ private static FpPoint simpleSwuG1(Fp u) {
+ var tv1 = G1_Z.mul(u.square());
+ var tv2 = tv1.square().add(tv1);
+ var tv3 = G1_B.mul(tv2.add(Fp.ONE));
+ var tv4 = (tv2.isZero() ? G1_Z : tv2.neg()).mul(G1_A);
+ var tv6 = tv4.square();
+ tv2 = tv3.square().add(G1_A.mul(tv6));
+ tv2 = tv2.mul(tv3);
+ tv6 = tv6.mul(tv4);
+ tv2 = tv2.add(G1_B.mul(tv6));
+ var x = tv1.mul(tv3);
+ var sqrtRatio = sqrtRatio(tv2, tv6, G1_Z);
+ var y = tv1.mul(u).mul(sqrtRatio.value());
+ if (sqrtRatio.wasSquare()) {
+ x = tv3;
+ y = sqrtRatio.value();
+ }
+ if (u.sgn0() != y.sgn0()) {
+ y = y.neg();
+ }
+ x = x.div(tv4);
+ return new FpPoint(x, y);
+ }
+
+ private static Fp2Point simpleSwuG2(Fp2 u) {
+ var tv1 = G2_Z.mul(u.square());
+ var tv2 = tv1.square().add(tv1);
+ var tv3 = G2_B.mul(tv2.add(Fp2.ONE));
+ var tv4 = (tv2.isZero() ? G2_Z : tv2.neg()).mul(G2_A);
+ var tv6 = tv4.square();
+ tv2 = tv3.square().add(G2_A.mul(tv6));
+ tv2 = tv2.mul(tv3);
+ tv6 = tv6.mul(tv4);
+ tv2 = tv2.add(G2_B.mul(tv6));
+ var x = tv1.mul(tv3);
+ var sqrtRatio = sqrtRatio(tv2, tv6, G2_Z);
+ var y = tv1.mul(u).mul(sqrtRatio.value());
+ if (sqrtRatio.wasSquare()) {
+ x = tv3;
+ y = sqrtRatio.value();
+ }
+ if (u.sgn0() != y.sgn0()) {
+ y = y.neg();
+ }
+ x = x.div(tv4);
+ return new Fp2Point(x, y);
+ }
+
+ private static G1Point isoMapG1(FpPoint point) {
+ var x = point.x();
+ var xNum = eval(G1_X_NUM, x);
+ var xDen = evalWithLeadingOne(G1_X_DEN, x);
+ var yNum = eval(G1_Y_NUM, x);
+ var yDen = evalWithLeadingOne(G1_Y_DEN, x);
+ if (xDen.isZero() || yDen.isZero()) {
+ return G1Point.INFINITY;
+ }
+ return new G1Point(xNum.div(xDen), point.y().mul(yNum).div(yDen));
+ }
+
+ private static G2Point isoMapG2(Fp2Point point) {
+ var x = point.x();
+ var xNum = eval(G2_X_NUM, x);
+ var xDen = evalWithLeadingOne(G2_X_DEN, x);
+ var yNum = eval(G2_Y_NUM, x);
+ var yDen = evalWithLeadingOne(G2_Y_DEN, x);
+ if (xDen.isZero() || yDen.isZero()) {
+ return G2Point.INFINITY;
+ }
+ return new G2Point(xNum.div(xDen), point.y().mul(yNum).div(yDen));
+ }
+
+ private static SqrtRatioFp sqrtRatio(Fp u, Fp v, Fp z) {
+ var direct = u.div(v).sqrt();
+ if (direct.isPresent()) {
+ return new SqrtRatioFp(true, direct.get());
+ }
+ return new SqrtRatioFp(false, z.mul(u).div(v).sqrt()
+ .orElseThrow(() -> new IllegalStateException("sqrt_ratio failed")));
+ }
+
+ private static SqrtRatioFp2 sqrtRatio(Fp2 u, Fp2 v, Fp2 z) {
+ var direct = u.div(v).sqrt();
+ if (direct.isPresent()) {
+ return new SqrtRatioFp2(true, direct.get());
+ }
+ return new SqrtRatioFp2(false, z.mul(u).div(v).sqrt()
+ .orElseThrow(() -> new IllegalStateException("sqrt_ratio failed")));
+ }
+
+ private static Fp eval(Fp[] coeffs, Fp x) {
+ var result = coeffs[coeffs.length - 1];
+ for (int i = coeffs.length - 2; i >= 0; i--) {
+ result = result.mul(x).add(coeffs[i]);
+ }
+ return result;
+ }
+
+ private static Fp evalWithLeadingOne(Fp[] coeffs, Fp x) {
+ var result = Fp.ONE;
+ for (int i = coeffs.length - 1; i >= 0; i--) {
+ result = result.mul(x).add(coeffs[i]);
+ }
+ return result;
+ }
+
+ private static Fp2 eval(Fp2[] coeffs, Fp2 x) {
+ var result = coeffs[coeffs.length - 1];
+ for (int i = coeffs.length - 2; i >= 0; i--) {
+ result = result.mul(x).add(coeffs[i]);
+ }
+ return result;
+ }
+
+ private static Fp2 evalWithLeadingOne(Fp2[] coeffs, Fp2 x) {
+ var result = Fp2.ONE;
+ for (int i = coeffs.length - 1; i >= 0; i--) {
+ result = result.mul(x).add(coeffs[i]);
+ }
+ return result;
+ }
+
+ private static Fp fp(String hex) {
+ Objects.requireNonNull(hex, "hex required");
+ return Fp.of(new BigInteger(hex, 16));
+ }
+
+ private static Fp[] fpArray(String... hexValues) {
+ var out = new Fp[hexValues.length];
+ for (int i = 0; i < hexValues.length; i++) {
+ out[i] = fp(hexValues[i]);
+ }
+ return out;
+ }
+
+ private static Fp2 fp2(String c0Hex, String c1Hex) {
+ return Fp2.of(fp(c0Hex), fp(c1Hex));
+ }
+
+ private static Fp2[] fp2Array(Fp2... values) {
+ return values;
+ }
+
+ private record FpPoint(Fp x, Fp y) {}
+ private record Fp2Point(Fp2 x, Fp2 y) {}
+ private record SqrtRatioFp(boolean wasSquare, Fp value) {}
+ private record SqrtRatioFp2(boolean wasSquare, Fp2 value) {}
+}
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G1Point.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G1Point.java
similarity index 64%
rename from zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G1Point.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G1Point.java
index a600b70..3199269 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G1Point.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G1Point.java
@@ -1,4 +1,6 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
+package com.bloxbean.cardano.zeroj.bls12381.ec;
+
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
import java.math.BigInteger;
@@ -8,13 +10,33 @@
public record G1Point(Fp x, Fp y) {
public static final G1Point INFINITY = new G1Point(null, null);
+ private static final Fp B = Fp.of(4);
/** Scalar field order r. */
public static final BigInteger R = new BigInteger(
"73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", 16);
+ public G1Point {
+ if ((x == null) != (y == null)) {
+ throw new IllegalArgumentException("G1 infinity must have both coordinates null");
+ }
+ }
+
public boolean isInfinity() { return x == null; }
+ public boolean isOnCurve() {
+ if (isInfinity()) return true;
+ return y.square().equals(x.square().mul(x).add(B));
+ }
+
+ public boolean isInSubgroup() {
+ return isInfinity() || scalarMul(R).isInfinity();
+ }
+
+ public boolean isValid() {
+ return isOnCurve() && isInSubgroup();
+ }
+
/** Create from snarkjs projective [x, y, z] coordinates. */
public static G1Point fromProjective(BigInteger px, BigInteger py, BigInteger pz) {
if (pz.signum() == 0) return INFINITY;
@@ -61,4 +83,19 @@ public G1Point scalarMul(BigInteger scalar) {
}
return result;
}
+
+ /**
+ * Fixed-schedule scalar multiplication for secret-scalar callers.
+ */
+ public G1Point ctScalarMul(BigInteger scalar) {
+ if (scalar.signum() == 0 || isInfinity()) return INFINITY;
+ if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate());
+ var affine = JacobianG1BLS381.fromAffine(x.value(), y.value())
+ .ctScalarMul(scalar)
+ .toAffine();
+ if (affine.isInfinity()) {
+ return INFINITY;
+ }
+ return new G1Point(Fp.of(affine.xBigInt()), Fp.of(affine.yBigInt()));
+ }
}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G2Point.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G2Point.java
new file mode 100644
index 0000000..5642b0b
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/G2Point.java
@@ -0,0 +1,108 @@
+package com.bloxbean.cardano.zeroj.bls12381.ec;
+
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+
+import java.math.BigInteger;
+
+/**
+ * BLS12-381 G2 point in affine coordinates on the twisted curve y^2 = x^3 + 4(1+u) over Fp2.
+ */
+public record G2Point(Fp2 x, Fp2 y) {
+
+ public static final G2Point INFINITY = new G2Point(null, null);
+ private static final Fp2 B_TWIST = Fp2.of(Fp.of(4), Fp.of(4));
+
+ public G2Point {
+ if ((x == null) != (y == null)) {
+ throw new IllegalArgumentException("G2 infinity must have both coordinates null");
+ }
+ }
+
+ public boolean isInfinity() { return x == null; }
+
+ public boolean isOnCurve() {
+ if (isInfinity()) return true;
+ return y.square().equals(x.square().mul(x).add(B_TWIST));
+ }
+
+ public boolean isInSubgroup() {
+ return isInfinity() || scalarMul(G1Point.R).isInfinity();
+ }
+
+ public boolean isValid() {
+ return isOnCurve() && isInSubgroup();
+ }
+
+ /** Create from snarkjs projective [[x_c0,x_c1],[y_c0,y_c1],[z_c0,z_c1]]. */
+ public static G2Point fromProjective(BigInteger xc0, BigInteger xc1,
+ BigInteger yc0, BigInteger yc1,
+ BigInteger zc0, BigInteger zc1) {
+ var z = Fp2.of(Fp.of(zc0), Fp.of(zc1));
+ if (z.isZero()) return INFINITY;
+ if (z.c0().equals(Fp.ONE) && z.c1().isZero()) {
+ return new G2Point(Fp2.of(Fp.of(xc0), Fp.of(xc1)), Fp2.of(Fp.of(yc0), Fp.of(yc1)));
+ }
+ var zInv = z.inv();
+ return new G2Point(
+ Fp2.of(Fp.of(xc0), Fp.of(xc1)).mul(zInv),
+ Fp2.of(Fp.of(yc0), Fp.of(yc1)).mul(zInv));
+ }
+
+ public G2Point negate() {
+ return isInfinity() ? this : new G2Point(x, y.neg());
+ }
+
+ public G2Point add(G2Point o) {
+ if (this.isInfinity()) return o;
+ if (o.isInfinity()) return this;
+ if (this.x.equals(o.x)) {
+ return this.y.equals(o.y) ? this.doublePoint() : INFINITY;
+ }
+ var lambda = o.y.sub(this.y).mul(o.x.sub(this.x).inv());
+ var x3 = lambda.square().sub(this.x).sub(o.x);
+ var y3 = lambda.mul(this.x.sub(x3)).sub(this.y);
+ return new G2Point(x3, y3);
+ }
+
+ public G2Point doublePoint() {
+ if (isInfinity() || y.isZero()) return INFINITY;
+ var three = Fp2.of(Fp.of(3), Fp.ZERO);
+ var lambda = x.square().mul(three).mul(y.add(y).inv());
+ var x3 = lambda.square().sub(x).sub(x);
+ var y3 = lambda.mul(x.sub(x3)).sub(y);
+ return new G2Point(x3, y3);
+ }
+
+ public G2Point scalarMul(BigInteger scalar) {
+ if (scalar.signum() == 0) return INFINITY;
+ if (scalar.signum() < 0) return negate().scalarMul(scalar.negate());
+ var result = INFINITY;
+ var base = this;
+ var s = scalar;
+ while (s.signum() > 0) {
+ if (s.testBit(0)) result = result.add(base);
+ base = base.doublePoint();
+ s = s.shiftRight(1);
+ }
+ return result;
+ }
+
+ /**
+ * Fixed-schedule scalar multiplication for secret-scalar callers.
+ */
+ public G2Point ctScalarMul(BigInteger scalar) {
+ if (scalar.signum() == 0 || isInfinity()) return INFINITY;
+ if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate());
+ var affine = JacobianG2BLS381.fromAffine(
+ MontFp2_381.of(x.c0().value(), x.c1().value()),
+ MontFp2_381.of(y.c0().value(), y.c1().value()))
+ .ctScalarMul(scalar)
+ .toAffine();
+ if (affine.isInfinity()) {
+ return INFINITY;
+ }
+ return new G2Point(
+ Fp2.of(Fp.of(affine.x().reBigInt()), Fp.of(affine.x().imBigInt())),
+ Fp2.of(Fp.of(affine.y().reBigInt()), Fp.of(affine.y().imBigInt())));
+ }
+}
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG1BLS381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381.java
similarity index 93%
rename from zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG1BLS381.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381.java
index 909d052..4f8fee3 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG1BLS381.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381.java
@@ -1,6 +1,6 @@
-package com.bloxbean.cardano.zeroj.crypto.ec;
+package com.bloxbean.cardano.zeroj.bls12381.ec;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
import java.math.BigInteger;
@@ -172,21 +172,20 @@ public JacobianG1BLS381 ctScalarMul(BigInteger scalar) {
if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate());
if (this.isInfinity()) return INFINITY;
- // Montgomery ladder: R0 = infinity, R1 = P
- // For each bit (MSB to LSB):
- // if bit=0: R1 = R0 + R1, R0 = 2*R0
- // if bit=1: R0 = R0 + R1, R1 = 2*R1
- // Always perform both operations to maintain constant-time behavior.
+ // Montgomery ladder with a fixed operation schedule per bit.
JacobianG1BLS381 r0 = INFINITY;
JacobianG1BLS381 r1 = this;
for (int i = 255; i >= 0; i--) {
+ JacobianG1BLS381 sum = r0.add(r1);
+ JacobianG1BLS381 double0 = r0.doublePoint();
+ JacobianG1BLS381 double1 = r1.doublePoint();
if (scalar.testBit(i)) {
- r0 = r0.add(r1);
- r1 = r1.doublePoint();
+ r0 = sum;
+ r1 = double1;
} else {
- r1 = r0.add(r1);
- r0 = r0.doublePoint();
+ r0 = double0;
+ r1 = sum;
}
}
return r0;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG2BLS381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java
similarity index 92%
rename from zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG2BLS381.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java
index 0fd0e89..0da4eb6 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG2BLS381.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java
@@ -1,7 +1,7 @@
-package com.bloxbean.cardano.zeroj.crypto.ec;
+package com.bloxbean.cardano.zeroj.bls12381.ec;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp2_381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp2_381;
import java.math.BigInteger;
@@ -134,12 +134,15 @@ public JacobianG2BLS381 ctScalarMul(BigInteger scalar) {
JacobianG2BLS381 r1 = this;
for (int i = 255; i >= 0; i--) {
+ JacobianG2BLS381 sum = r0.add(r1);
+ JacobianG2BLS381 double0 = r0.doublePoint();
+ JacobianG2BLS381 double1 = r1.doublePoint();
if (scalar.testBit(i)) {
- r0 = r0.add(r1);
- r1 = r1.doublePoint();
+ r0 = sum;
+ r1 = double1;
} else {
- r1 = r0.add(r1);
- r0 = r0.doublePoint();
+ r0 = double0;
+ r1 = sum;
}
}
return r0;
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp.java
similarity index 70%
rename from zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp.java
index afaca22..8314bab 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp.java
@@ -1,6 +1,7 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import java.math.BigInteger;
+import java.util.Optional;
/**
* BLS12-381 base field element — arithmetic modulo the base field prime p.
@@ -12,6 +13,8 @@ public record Fp(BigInteger value) {
/** BLS12-381 base field prime (381 bits). */
public static final BigInteger P = new BigInteger(
"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 16);
+ private static final BigInteger SQRT_EXPONENT = P.add(BigInteger.ONE).shiftRight(2);
+ private static final BigInteger LEXICOGRAPHIC_LIMIT = P.subtract(BigInteger.ONE).shiftRight(1);
public static final Fp ZERO = new Fp(BigInteger.ZERO);
public static final Fp ONE = new Fp(BigInteger.ONE);
@@ -25,11 +28,18 @@ public record Fp(BigInteger value) {
public Fp add(Fp o) { return new Fp(value.add(o.value)); }
public Fp sub(Fp o) { return new Fp(value.subtract(o.value).add(P)); }
public Fp mul(Fp o) { return new Fp(value.multiply(o.value)); }
+ public Fp div(Fp o) { return mul(o.inv()); }
public Fp neg() { return value.signum() == 0 ? this : new Fp(P.subtract(value)); }
public Fp inv() { return new Fp(value.modInverse(P)); }
public Fp square() { return new Fp(value.multiply(value)); }
public Fp pow(BigInteger exp) { return new Fp(value.modPow(exp, P)); }
+ public Optional sqrt() {
+ var root = pow(SQRT_EXPONENT);
+ return root.square().equals(this) ? Optional.of(root) : Optional.empty();
+ }
public boolean isZero() { return value.signum() == 0; }
+ public boolean sgn0() { return value.testBit(0); }
+ public boolean lexicographicallyLargest() { return value.compareTo(LEXICOGRAPHIC_LIMIT) > 0; }
@Override public String toString() { return value.toString(); }
}
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp12.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp12.java
similarity index 96%
rename from zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp12.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp12.java
index adbba59..2bdcedd 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp12.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp12.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import java.math.BigInteger;
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp2.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2.java
similarity index 52%
rename from zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp2.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2.java
index 5358757..70ba331 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp2.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2.java
@@ -1,4 +1,7 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
+
+import java.math.BigInteger;
+import java.util.Optional;
/**
* BLS12-381 quadratic extension field element (Fp2 = Fp[u] / (u^2 + 1)).
@@ -7,6 +10,9 @@
*/
public record Fp2(Fp c0, Fp c1) {
+ private static final BigInteger P_MINUS_THREE_OVER_FOUR = Fp.P.subtract(BigInteger.valueOf(3)).shiftRight(2);
+ private static final BigInteger P_MINUS_ONE_OVER_TWO = Fp.P.subtract(BigInteger.ONE).shiftRight(1);
+
public static final Fp2 ZERO = new Fp2(Fp.ZERO, Fp.ZERO);
public static final Fp2 ONE = new Fp2(Fp.ONE, Fp.ZERO);
@@ -32,7 +38,34 @@ public Fp2 square() {
public Fp2 conjugate() { return new Fp2(c0, c1.neg()); }
public Fp norm() { return c0.square().add(c1.square()); }
public Fp2 inv() { var n = norm().inv(); return new Fp2(c0.mul(n), c1.neg().mul(n)); }
+ public Fp2 div(Fp2 o) { return mul(o.inv()); }
public Fp2 mulScalar(Fp s) { return new Fp2(c0.mul(s), c1.mul(s)); }
+ public Fp2 pow(BigInteger exp) {
+ if (exp.signum() < 0) return inv().pow(exp.negate());
+ var result = ONE;
+ var base = this;
+ var e = exp;
+ while (e.signum() > 0) {
+ if (e.testBit(0)) result = result.mul(base);
+ base = base.square();
+ e = e.shiftRight(1);
+ }
+ return result;
+ }
+ public Optional sqrt() {
+ if (isZero()) return Optional.of(ZERO);
+
+ var a1 = pow(P_MINUS_THREE_OVER_FOUR);
+ var alpha = a1.square().mul(this);
+ var x0 = a1.mul(this);
+ Fp2 root;
+ if (alpha.equals(ONE.neg())) {
+ root = new Fp2(x0.c1.neg(), x0.c0);
+ } else {
+ root = alpha.add(ONE).pow(P_MINUS_ONE_OVER_TWO).mul(x0);
+ }
+ return root.square().equals(this) ? Optional.of(root) : Optional.empty();
+ }
/**
* Multiply by the non-residue xi = 1 + u used for BLS12-381 Fp6 tower.
@@ -43,6 +76,10 @@ public Fp2 mulByNonResidue() {
}
public boolean isZero() { return c0.isZero() && c1.isZero(); }
+ public boolean sgn0() { return c0.sgn0() || (c0.isZero() && c1.sgn0()); }
+ public boolean lexicographicallyLargest() {
+ return c1.lexicographicallyLargest() || (c1.isZero() && c0.lexicographicallyLargest());
+ }
@Override public String toString() { return "(" + c0 + " + " + c1 + "*u)"; }
}
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp6.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6.java
similarity index 96%
rename from zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp6.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6.java
index 1d67086..075d43f 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/Fp6.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
/**
* BLS12-381 sextic extension field element (Fp6 = Fp2[v] / (v^3 - xi)).
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp2_381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp2_381.java
similarity index 98%
rename from zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp2_381.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp2_381.java
index 6d87e01..5edabe5 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp2_381.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp2_381.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java
similarity index 99%
rename from zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp381.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java
index 4c74ce0..325f2d2 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp381.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFr381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java
similarity index 99%
rename from zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFr381.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java
index 5f3f5a6..cc2332e 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/field/MontFr381.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import java.math.BigInteger;
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontUtil.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontUtil.java
new file mode 100644
index 0000000..422c175
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontUtil.java
@@ -0,0 +1,40 @@
+package com.bloxbean.cardano.zeroj.bls12381.field;
+
+import java.math.BigInteger;
+
+/**
+ * Shared utilities for Montgomery-form field element conversions.
+ */
+final class MontUtil {
+
+ private MontUtil() {}
+
+ /**
+ * Convert 4 unsigned 64-bit limbs (little-endian) to BigInteger.
+ */
+ static BigInteger limbsToBigInteger(long l0, long l1, long l2, long l3) {
+ BigInteger result = toUnsignedBigInteger(l3);
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l2));
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l1));
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l0));
+ return result;
+ }
+
+ /**
+ * Convert 6 unsigned 64-bit limbs (little-endian) to BigInteger.
+ */
+ static BigInteger limbsToBigInteger(long l0, long l1, long l2, long l3, long l4, long l5) {
+ BigInteger result = toUnsignedBigInteger(l5);
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l4));
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l3));
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l2));
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l1));
+ result = result.shiftLeft(64).or(toUnsignedBigInteger(l0));
+ return result;
+ }
+
+ static BigInteger toUnsignedBigInteger(long v) {
+ if (v >= 0) return BigInteger.valueOf(v);
+ return BigInteger.valueOf(v >>> 1).shiftLeft(1).or(BigInteger.valueOf(v & 1));
+ }
+}
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/BLS12381Pairing.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381Pairing.java
similarity index 97%
rename from zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/BLS12381Pairing.java
rename to zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381Pairing.java
index 45b3ac2..544ec38 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/BLS12381Pairing.java
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381Pairing.java
@@ -1,4 +1,7 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
+package com.bloxbean.cardano.zeroj.bls12381.pairing;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
import java.math.BigInteger;
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java
new file mode 100644
index 0000000..5d63a89
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Provider.java
@@ -0,0 +1,168 @@
+package com.bloxbean.cardano.zeroj.bls12381.spi;
+
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Hash;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * Provider boundary for BLS12-381 primitive implementations.
+ *
+ * Scalar multiplication methods reduce the scalar modulo the BLS12-381
+ * scalar-field order. They are for public scalars. Protocol code that multiplies
+ * by secret scalars must use a backend with an explicit constant-time contract.
+ */
+public interface Bls12381Provider {
+ String id();
+
+ G1Point g1Generator();
+
+ G2Point g2Generator();
+
+ default G1Point g1Identity() {
+ return G1Point.INFINITY;
+ }
+
+ default G2Point g2Identity() {
+ return G2Point.INFINITY;
+ }
+
+ default G1Point g1Add(G1Point left, G1Point right) {
+ return Bls12381Codecs.requireValid(left).add(Bls12381Codecs.requireValid(right));
+ }
+
+ default G2Point g2Add(G2Point left, G2Point right) {
+ return Bls12381Codecs.requireValid(left).add(Bls12381Codecs.requireValid(right));
+ }
+
+ default G1Point g1Negate(G1Point point) {
+ return Bls12381Codecs.requireValid(point).negate();
+ }
+
+ default G2Point g2Negate(G2Point point) {
+ return Bls12381Codecs.requireValid(point).negate();
+ }
+
+ G1Point g1ScalarMul(G1Point point, BigInteger scalar);
+
+ G2Point g2ScalarMul(G2Point point, BigInteger scalar);
+
+ /**
+ * Multiply a G1 point by a secret scalar using this provider's side-channel hardened path.
+ */
+ default G1Point g1SecretScalarMul(G1Point point, BigInteger scalar) {
+ throw new UnsupportedOperationException(id() + " does not declare a secret-scalar G1 multiplication contract");
+ }
+
+ /**
+ * Multiply a G2 point by a secret scalar using this provider's side-channel hardened path.
+ */
+ default G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) {
+ throw new UnsupportedOperationException(id() + " does not declare a secret-scalar G2 multiplication contract");
+ }
+
+ boolean pairingProductIsIdentity(G1Point[] g1Points, G2Point[] g2Points);
+
+ default boolean g1IsValid(G1Point point) {
+ return point != null && point.isValid();
+ }
+
+ default boolean g2IsValid(G2Point point) {
+ return point != null && point.isValid();
+ }
+
+ default byte[] g1ToCompressed(G1Point point) {
+ return Bls12381Codecs.g1ToCompressed(Bls12381Codecs.requireValid(point));
+ }
+
+ default G1Point g1FromCompressed(byte[] bytes) {
+ return Bls12381Codecs.g1FromCompressed(bytes);
+ }
+
+ default byte[] g1ToUncompressed(G1Point point) {
+ return Bls12381Codecs.g1ToUncompressed(Bls12381Codecs.requireValid(point));
+ }
+
+ default G1Point g1FromUncompressed(byte[] bytes) {
+ return Bls12381Codecs.g1FromUncompressed(bytes);
+ }
+
+ default byte[] g2ToCompressed(G2Point point) {
+ return Bls12381Codecs.g2ToCompressed(Bls12381Codecs.requireValid(point));
+ }
+
+ default G2Point g2FromCompressed(byte[] bytes) {
+ return Bls12381Codecs.g2FromCompressed(bytes);
+ }
+
+ default byte[] g2ToUncompressed(G2Point point) {
+ return Bls12381Codecs.g2ToUncompressed(Bls12381Codecs.requireValid(point));
+ }
+
+ default G2Point g2FromUncompressed(byte[] bytes) {
+ return Bls12381Codecs.g2FromUncompressed(bytes);
+ }
+
+ default BigInteger hashToScalar(byte[] message, byte[] dst) {
+ Objects.requireNonNull(message, "message required");
+ Objects.requireNonNull(dst, "dst required");
+ return Bls12381Hash.hashToScalar(message, dst);
+ }
+
+ default BigInteger hashToScalarXofShake256(byte[] message, byte[] dst) {
+ Objects.requireNonNull(message, "message required");
+ Objects.requireNonNull(dst, "dst required");
+ return Bls12381Hash.hashToScalarXofShake256(message, dst);
+ }
+
+ default G1Point g1HashToCurve(byte[] message, byte[] dst) {
+ return Bls12381Hash.hashToG1(message, dst);
+ }
+
+ default G1Point g1EncodeToCurve(byte[] message, byte[] dst) {
+ return Bls12381Hash.encodeToG1(message, dst);
+ }
+
+ default G1Point g1HashToCurveXofShake256(byte[] message, byte[] dst) {
+ return Bls12381Hash.hashToG1XofShake256(message, dst);
+ }
+
+ default G1Point g1EncodeToCurveXofShake256(byte[] message, byte[] dst) {
+ return Bls12381Hash.encodeToG1XofShake256(message, dst);
+ }
+
+ default G2Point g2HashToCurve(byte[] message, byte[] dst) {
+ return Bls12381Hash.hashToG2(message, dst);
+ }
+
+ default G2Point g2EncodeToCurve(byte[] message, byte[] dst) {
+ return Bls12381Hash.encodeToG2(message, dst);
+ }
+
+ default G2Point g2HashToCurveXofShake256(byte[] message, byte[] dst) {
+ return Bls12381Hash.hashToG2XofShake256(message, dst);
+ }
+
+ default G2Point g2EncodeToCurveXofShake256(byte[] message, byte[] dst) {
+ return Bls12381Hash.encodeToG2XofShake256(message, dst);
+ }
+
+ default G1Point g1ScalarMulGenerator(BigInteger scalar) {
+ return g1ScalarMul(g1Generator(), scalar);
+ }
+
+ default G2Point g2ScalarMulGenerator(BigInteger scalar) {
+ return g2ScalarMul(g2Generator(), scalar);
+ }
+
+ default G1Point g1SecretScalarMulGenerator(BigInteger scalar) {
+ return g1SecretScalarMul(g1Generator(), scalar);
+ }
+
+ default G2Point g2SecretScalarMulGenerator(BigInteger scalar) {
+ return g2SecretScalarMul(g2Generator(), scalar);
+ }
+}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Providers.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Providers.java
new file mode 100644
index 0000000..53b8a10
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/Bls12381Providers.java
@@ -0,0 +1,12 @@
+package com.bloxbean.cardano.zeroj.bls12381.spi;
+
+/**
+ * Built-in BLS12-381 provider factories.
+ */
+public final class Bls12381Providers {
+ private Bls12381Providers() {}
+
+ public static Bls12381Provider pureJava() {
+ return PureJavaBls12381Provider.INSTANCE;
+ }
+}
diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java
new file mode 100644
index 0000000..d274d42
--- /dev/null
+++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381Provider.java
@@ -0,0 +1,76 @@
+package com.bloxbean.cardano.zeroj.bls12381.spi;
+
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators;
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * Default provider backed by ZeroJ's pure Java BLS12-381 implementation.
+ */
+public final class PureJavaBls12381Provider implements Bls12381Provider {
+ public static final PureJavaBls12381Provider INSTANCE = new PureJavaBls12381Provider();
+
+ private PureJavaBls12381Provider() {}
+
+ @Override
+ public String id() {
+ return "zeroj-bls12381-pure-java";
+ }
+
+ @Override
+ public G1Point g1Generator() {
+ return Bls12381Generators.G1;
+ }
+
+ @Override
+ public G2Point g2Generator() {
+ return Bls12381Generators.G2;
+ }
+
+ @Override
+ public G1Point g1ScalarMul(G1Point point, BigInteger scalar) {
+ return Bls12381Codecs.requireValid(point)
+ .scalarMul(reduceScalar(scalar));
+ }
+
+ @Override
+ public G2Point g2ScalarMul(G2Point point, BigInteger scalar) {
+ return Bls12381Codecs.requireValid(point)
+ .scalarMul(reduceScalar(scalar));
+ }
+
+ @Override
+ public G1Point g1SecretScalarMul(G1Point point, BigInteger scalar) {
+ return Bls12381Codecs.requireValid(point)
+ .ctScalarMul(reduceScalar(scalar));
+ }
+
+ @Override
+ public G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) {
+ return Bls12381Codecs.requireValid(point)
+ .ctScalarMul(reduceScalar(scalar));
+ }
+
+ @Override
+ public boolean pairingProductIsIdentity(G1Point[] g1Points, G2Point[] g2Points) {
+ Objects.requireNonNull(g1Points, "g1Points required");
+ Objects.requireNonNull(g2Points, "g2Points required");
+ for (G1Point point : g1Points) {
+ Bls12381Codecs.requireValid(point);
+ }
+ for (G2Point point : g2Points) {
+ Bls12381Codecs.requireValid(point);
+ }
+ return BLS12381Pairing.pairingCheck(g1Points, g2Points);
+ }
+
+ private static BigInteger reduceScalar(BigInteger scalar) {
+ return Objects.requireNonNull(scalar, "scalar required")
+ .mod(Bls12381Generators.SCALAR_FIELD_ORDER);
+ }
+}
diff --git a/zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/reflect-config.json b/zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/reflect-config.json
new file mode 100644
index 0000000..b787474
--- /dev/null
+++ b/zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/reflect-config.json
@@ -0,0 +1,110 @@
+[
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.Bls12381Codecs",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.Bls12381Hash",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.ec.G1Point",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.ec.G2Point",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381$AffineG1",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381$AffineG2",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.Fp",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.Fp2",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.Fp6",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.Fp12",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.MontFp381",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.MontFp2_381",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.field.MontFr381",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.bls12381.spi.PureJavaBls12381Provider",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ }
+]
diff --git a/zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/resource-config.json b/zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/resource-config.json
new file mode 100644
index 0000000..bf2f940
--- /dev/null
+++ b/zeroj-bls12381/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-bls12381/resource-config.json
@@ -0,0 +1,5 @@
+{
+ "resources": {
+ "includes": []
+ }
+}
diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java
new file mode 100644
index 0000000..1f1d9eb
--- /dev/null
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381CodecsTest.java
@@ -0,0 +1,135 @@
+package com.bloxbean.cardano.zeroj.bls12381;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class Bls12381CodecsTest {
+
+ @Test
+ void g1Uncompressed_roundTripsGenerator() {
+ byte[] encoded = Bls12381Codecs.g1ToUncompressed(Bls12381Generators.G1);
+ assertEquals(Bls12381Codecs.G1_UNCOMPRESSED_BYTES, encoded.length);
+ assertEquals(Bls12381Generators.G1, Bls12381Codecs.g1FromUncompressed(encoded));
+ }
+
+ @Test
+ void g1Compressed_roundTripsGenerator() {
+ byte[] encoded = Bls12381Codecs.g1ToCompressed(Bls12381Generators.G1);
+ assertEquals(Bls12381Codecs.G1_COMPRESSED_BYTES, encoded.length);
+ assertEquals(Bls12381Generators.G1, Bls12381Codecs.g1FromCompressed(encoded));
+ }
+
+ @Test
+ void g1Compressed_generatorMatchesZcashEncoding() {
+ assertArrayEquals(
+ hexToBytes("97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"),
+ Bls12381Codecs.g1ToCompressed(Bls12381Generators.G1));
+ }
+
+ @Test
+ void g2Uncompressed_roundTripsGenerator() {
+ byte[] encoded = Bls12381Codecs.g2ToUncompressed(Bls12381Generators.G2);
+ assertEquals(Bls12381Codecs.G2_UNCOMPRESSED_BYTES, encoded.length);
+ assertEquals(Bls12381Generators.G2, Bls12381Codecs.g2FromUncompressed(encoded));
+ }
+
+ @Test
+ void g2Compressed_roundTripsGenerator() {
+ byte[] encoded = Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2);
+ assertEquals(Bls12381Codecs.G2_COMPRESSED_BYTES, encoded.length);
+ assertEquals(Bls12381Generators.G2, Bls12381Codecs.g2FromCompressed(encoded));
+ }
+
+ @Test
+ void g2Compressed_generatorMatchesZcashEncoding() {
+ assertArrayEquals(
+ hexToBytes("93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"),
+ Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2));
+ }
+
+ @Test
+ void infinity_roundTrips() {
+ assertEquals(G1Point.INFINITY, Bls12381Codecs.g1FromUncompressed(Bls12381Codecs.g1ToUncompressed(G1Point.INFINITY)));
+ assertEquals(G2Point.INFINITY, Bls12381Codecs.g2FromUncompressed(Bls12381Codecs.g2ToUncompressed(G2Point.INFINITY)));
+ assertEquals(G1Point.INFINITY, Bls12381Codecs.g1FromCompressed(Bls12381Codecs.g1ToCompressed(G1Point.INFINITY)));
+ assertEquals(G2Point.INFINITY, Bls12381Codecs.g2FromCompressed(Bls12381Codecs.g2ToCompressed(G2Point.INFINITY)));
+ }
+
+ @Test
+ void scalarToLittleEndian32_encodesOne() {
+ byte[] scalar = Bls12381Codecs.scalarToLittleEndian32(BigInteger.ONE);
+ assertEquals(1, scalar[0]);
+ for (int i = 1; i < scalar.length; i++) {
+ assertEquals(0, scalar[i]);
+ }
+ }
+
+ @Test
+ void scalarToLittleEndian32Reduced_reducesSignedScalars() {
+ assertArrayEquals(
+ Bls12381Codecs.scalarToLittleEndian32(BigInteger.ZERO),
+ Bls12381Codecs.scalarToLittleEndian32Reduced(Bls12381Generators.SCALAR_FIELD_ORDER));
+ assertArrayEquals(
+ Bls12381Codecs.scalarToLittleEndian32(Bls12381Generators.SCALAR_FIELD_ORDER.subtract(BigInteger.ONE)),
+ Bls12381Codecs.scalarToLittleEndian32Reduced(BigInteger.valueOf(-1)));
+ }
+
+ @Test
+ void g1Uncompressed_rejectsOffCurvePoint() {
+ byte[] encoded = Bls12381Codecs.g1ToUncompressed(Bls12381Generators.G1);
+ encoded[encoded.length - 1] ^= 1;
+
+ assertThrows(IllegalArgumentException.class, () -> Bls12381Codecs.g1FromUncompressed(encoded));
+ assertFalse(Bls12381Codecs.g1FromUncompressedUnchecked(encoded).isOnCurve());
+ }
+
+ @Test
+ void g1Codecs_rejectPrimeFieldTorsionPoint() {
+ var torsion = new G1Point(Fp.ZERO, Fp.of(2));
+ assertTrue(torsion.isOnCurve());
+ assertFalse(torsion.isInSubgroup());
+
+ assertThrows(IllegalArgumentException.class,
+ () -> Bls12381Codecs.g1FromUncompressed(Bls12381Codecs.g1ToUncompressed(torsion)));
+ assertThrows(IllegalArgumentException.class,
+ () -> Bls12381Codecs.g1FromCompressed(Bls12381Codecs.g1ToCompressed(torsion)));
+ }
+
+ @Test
+ void g2Uncompressed_rejectsOffCurvePoint() {
+ byte[] encoded = Bls12381Codecs.g2ToUncompressed(Bls12381Generators.G2);
+ encoded[encoded.length - 1] ^= 1;
+
+ assertThrows(IllegalArgumentException.class, () -> Bls12381Codecs.g2FromUncompressed(encoded));
+ assertFalse(Bls12381Codecs.g2FromUncompressedUnchecked(encoded).isOnCurve());
+ }
+
+ @Test
+ void g2Codecs_rejectTwistTorsionPoint() {
+ var x = Fp2.of(Fp.ZERO, Fp.ONE);
+ var y = x.square().mul(x).add(Fp2.of(Fp.of(4), Fp.of(4))).sqrt().orElseThrow();
+ var torsion = new G2Point(x, y);
+ assertTrue(torsion.isOnCurve());
+ assertFalse(torsion.isInSubgroup());
+
+ assertThrows(IllegalArgumentException.class,
+ () -> Bls12381Codecs.g2FromUncompressed(Bls12381Codecs.g2ToUncompressed(torsion)));
+ assertThrows(IllegalArgumentException.class,
+ () -> Bls12381Codecs.g2FromCompressed(Bls12381Codecs.g2ToCompressed(torsion)));
+ }
+
+ 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-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java
new file mode 100644
index 0000000..057b366
--- /dev/null
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/Bls12381HashToCurveTest.java
@@ -0,0 +1,128 @@
+package com.bloxbean.cardano.zeroj.bls12381;
+
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class Bls12381HashToCurveTest {
+ private static final byte[] G1_RO_DST =
+ "QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] G2_RO_DST =
+ "QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] G1_NU_DST =
+ "QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_NU_".getBytes(StandardCharsets.US_ASCII);
+ private static final byte[] G2_NU_DST =
+ "QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_NU_".getBytes(StandardCharsets.US_ASCII);
+
+ @Test
+ void hashToG1_matchesRfc9380EmptyMessageVector() {
+ assertG1(
+ Bls12381Hash.hashToG1(new byte[0], G1_RO_DST),
+ "052926add2207b76ca4fa57a8734416c8dc95e24501772c814278700eed6d1e4e8cf62d9c09db0fac349612b759e79a1",
+ "08ba738453bfed09cb546dbb0783dbb3a5f1f566ed67bb6be0e8c67e2e81a4cc68ee29813bb7994998f3eae0c9c6a265");
+ }
+
+ @Test
+ void hashToG1_matchesRfc9380AbcVector() {
+ assertG1(
+ Bls12381Hash.hashToG1("abc".getBytes(StandardCharsets.US_ASCII), G1_RO_DST),
+ "03567bc5ef9c690c2ab2ecdf6a96ef1c139cc0b2f284dca0a9a7943388a49a3aee664ba5379a7655d3c68900be2f6903",
+ "0b9c15f3fe6e5cf4211f346271d7b01c8f3b28be689c8429c85b67af215533311f0b8dfaaa154fa6b88176c229f2885d");
+ }
+
+ @Test
+ void encodeToG1_matchesRfc9380AbcVector() {
+ assertG1(
+ Bls12381Hash.encodeToG1("abc".getBytes(StandardCharsets.US_ASCII), G1_NU_DST),
+ "009769f3ab59bfd551d53a5f846b9984c59b97d6842b20a2c565baa167945e3d026a3755b6345df8ec7e6acb6868ae6d",
+ "1532c00cf61aa3d0ce3e5aa20c3b531a2abd2c770a790a2613818303c6b830ffc0ecf6c357af3317b9575c567f11cd2c");
+ }
+
+ @Test
+ void expandMessageXmd_allowsZeroLengthOutput() {
+ assertArrayEquals(new byte[0],
+ Bls12381Hash.expandMessageXmdSha256("abc".getBytes(StandardCharsets.US_ASCII), G1_RO_DST, 0));
+ }
+
+ @Test
+ void expandMessageXmd_usesOversizeDstReduction() {
+ byte[] msg = "abc".getBytes(StandardCharsets.US_ASCII);
+ byte[] oversizedDst = ("QUUX-V01-CS02-with-expander-SHA256-128-long-DST-" + "1".repeat(256))
+ .getBytes(StandardCharsets.US_ASCII);
+ byte[] reducedDst = sha256(concat("H2C-OVERSIZE-DST-".getBytes(StandardCharsets.US_ASCII), oversizedDst));
+
+ assertArrayEquals(
+ Bls12381Hash.expandMessageXmdSha256(msg, reducedDst, 32),
+ Bls12381Hash.expandMessageXmdSha256(msg, oversizedDst, 32));
+ }
+
+ @Test
+ void hashToG2_matchesRfc9380EmptyMessageVector() {
+ assertG2(
+ Bls12381Hash.hashToG2(new byte[0], G2_RO_DST),
+ "0141ebfbdca40eb85b87142e130ab689c673cf60f1a3e98d69335266f30d9b8d4ac44c1038e9dcdd5393faf5c41fb78a",
+ "05cb8437535e20ecffaef7752baddf98034139c38452458baeefab379ba13dff5bf5dd71b72418717047f5b0f37da03d",
+ "0503921d7f6a12805e72940b963c0cf3471c7b2a524950ca195d11062ee75ec076daf2d4bc358c4b190c0c98064fdd92",
+ "12424ac32561493f3fe3c260708a12b7c620e7be00099a974e259ddc7d1f6395c3c811cdd19f1e8dbf3e9ecfdcbab8d6");
+ }
+
+ @Test
+ void hashToG2_matchesRfc9380AbcVector() {
+ assertG2(
+ Bls12381Hash.hashToG2("abc".getBytes(StandardCharsets.US_ASCII), G2_RO_DST),
+ "02c2d18e033b960562aae3cab37a27ce00d80ccd5ba4b7fe0e7a210245129dbec7780ccc7954725f4168aff2787776e6",
+ "139cddbccdc5e91b9623efd38c49f81a6f83f175e80b06fc374de9eb4b41dfe4ca3a230ed250fbe3a2acf73a41177fd8",
+ "1787327b68159716a37440985269cf584bcb1e621d3a7202be6ea05c4cfe244aeb197642555a0645fb87bf7466b2ba48",
+ "00aa65dae3c8d732d10ecd2c50f8a1baf3001578f71c694e03866e9f3d49ac1e1ce70dd94a733534f106d4cec0eddd16");
+ }
+
+ @Test
+ void encodeToG2_matchesRfc9380AbcVector() {
+ assertG2(
+ Bls12381Hash.encodeToG2("abc".getBytes(StandardCharsets.US_ASCII), G2_NU_DST),
+ "108ed59fd9fae381abfd1d6bce2fd2fa220990f0f837fa30e0f27914ed6e1454db0d1ee957b219f61da6ff8be0d6441f",
+ "0296238ea82c6d4adb3c838ee3cb2346049c90b96d602d7bb1b469b905c9228be25c627bffee872def773d5b2a2eb57d",
+ "033f90f6057aadacae7963b0a0b379dd46750c1c94a6357c99b65f63b79e321ff50fe3053330911c56b6ceea08fee656",
+ "153606c417e59fb331b7ae6bce4fbf7c5190c33ce9402b5ebe2b70e44fca614f3f1382a3625ed5493843d0b0a652fc3f");
+ }
+
+ private static void assertG1(G1Point point, String x, String y) {
+ assertTrue(point.isValid());
+ assertEquals(fp(x), point.x());
+ assertEquals(fp(y), point.y());
+ }
+
+ private static void assertG2(G2Point point, String xc0, String xc1, String yc0, String yc1) {
+ assertTrue(point.isValid());
+ assertEquals(Fp2.of(fp(xc0), fp(xc1)), point.x());
+ assertEquals(Fp2.of(fp(yc0), fp(yc1)), point.y());
+ }
+
+ private static Fp fp(String hex) {
+ return Fp.of(new BigInteger(hex, 16));
+ }
+
+ private static byte[] sha256(byte[] input) {
+ try {
+ return MessageDigest.getInstance("SHA-256").digest(input);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static byte[] concat(byte[] left, byte[] right) {
+ byte[] out = Arrays.copyOf(left, left.length + right.length);
+ System.arraycopy(right, 0, out, left.length, right.length);
+ return out;
+ }
+}
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG1BLS381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java
similarity index 89%
rename from zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG1BLS381Test.java
rename to zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java
index 676ea34..71fbc77 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG1BLS381Test.java
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java
@@ -1,6 +1,6 @@
-package com.bloxbean.cardano.zeroj.crypto.ec;
+package com.bloxbean.cardano.zeroj.bls12381.ec;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
@@ -87,9 +87,9 @@ void ctScalarMul_byOrder_returnsInfinity() {
@Test
void crossValidate_withVerifierG1Point() {
var g = JacobianG1BLS381.GENERATOR.toAffine();
- var verG1 = new com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.G1Point(
- com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.of(g.xBigInt()),
- com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.of(g.yBigInt()));
+ var verG1 = new com.bloxbean.cardano.zeroj.bls12381.ec.G1Point(
+ com.bloxbean.cardano.zeroj.bls12381.field.Fp.of(g.xBigInt()),
+ com.bloxbean.cardano.zeroj.bls12381.field.Fp.of(g.yBigInt()));
// Double using verifier
var verDbl = verG1.doublePoint();
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG2BLS381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java
similarity index 97%
rename from zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG2BLS381Test.java
rename to zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java
index f913e04..707b66f 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/ec/JacobianG2BLS381Test.java
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.ec;
+package com.bloxbean.cardano.zeroj.bls12381.ec;
import org.junit.jupiter.api.Test;
diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2Test.java
new file mode 100644
index 0000000..1eac0cb
--- /dev/null
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp2Test.java
@@ -0,0 +1,18 @@
+package com.bloxbean.cardano.zeroj.bls12381.field;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class Fp2Test {
+
+ @Test
+ void sqrtMinusOne_usesFp2SpecialCaseBranch() {
+ Fp2 minusOne = Fp2.ONE.neg();
+
+ Fp2 root = minusOne.sqrt().orElseThrow();
+
+ assertEquals(minusOne, root.square());
+ assertTrue(root.equals(Fp2.of(Fp.ZERO, Fp.ONE)) || root.equals(Fp2.of(Fp.ZERO, Fp.ONE.neg())));
+ }
+}
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp2_381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp2_381Test.java
similarity index 83%
rename from zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp2_381Test.java
rename to zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp2_381Test.java
index 8c4c395..83dcc5a 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp2_381Test.java
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp2_381Test.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.RepeatedTest;
@@ -93,13 +93,13 @@ void crossValidate_withVerifierFp2() {
var montA = MontFp2_381.of(a0, a1);
var montB = MontFp2_381.of(b0, b1);
- var verFp = com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.class;
- var verA = com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp2.of(
- com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.of(a0),
- com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.of(a1));
- var verB = com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp2.of(
- com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.of(b0),
- com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.of(b1));
+ var verFp = com.bloxbean.cardano.zeroj.bls12381.field.Fp.class;
+ var verA = com.bloxbean.cardano.zeroj.bls12381.field.Fp2.of(
+ com.bloxbean.cardano.zeroj.bls12381.field.Fp.of(a0),
+ com.bloxbean.cardano.zeroj.bls12381.field.Fp.of(a1));
+ var verB = com.bloxbean.cardano.zeroj.bls12381.field.Fp2.of(
+ com.bloxbean.cardano.zeroj.bls12381.field.Fp.of(b0),
+ com.bloxbean.cardano.zeroj.bls12381.field.Fp.of(b1));
var montResult = montA.mul(montB);
var verResult = verA.mul(verB);
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java
similarity index 93%
rename from zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp381Test.java
rename to zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java
index 12f41b5..4e6f576 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFp381Test.java
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.RepeatedTest;
@@ -114,15 +114,15 @@ void square_random_matchesMul() {
@Test
void crossValidate_withVerifierFp() {
// Cross-validate against the verifier's BigInteger-based Fp
- var vFp = com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp.class;
+ var vFp = com.bloxbean.cardano.zeroj.bls12381.field.Fp.class;
BigInteger a = new BigInteger("123456789012345678901234567890123456789012345678901234567890");
BigInteger b = new BigInteger("987654321098765432109876543210987654321098765432109876543210");
var montA = MontFp381.fromBigInteger(a);
var montB = MontFp381.fromBigInteger(b);
- var verA = new com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp(a);
- var verB = new com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.Fp(b);
+ var verA = new com.bloxbean.cardano.zeroj.bls12381.field.Fp(a);
+ var verB = new com.bloxbean.cardano.zeroj.bls12381.field.Fp(b);
assertEquals(verA.mul(verB).value(), montA.mul(montB).toBigInteger(), "mul cross-validate");
assertEquals(verA.add(verB).value(), montA.add(montB).toBigInteger(), "add cross-validate");
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFr381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java
similarity index 98%
rename from zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFr381Test.java
rename to zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java
index e28f972..67c155e 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/field/MontFr381Test.java
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java
@@ -1,4 +1,4 @@
-package com.bloxbean.cardano.zeroj.crypto.field;
+package com.bloxbean.cardano.zeroj.bls12381.field;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.RepeatedTest;
diff --git a/zeroj-verifier-groth16/src/test/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/BLS12381PairingTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java
similarity index 94%
rename from zeroj-verifier-groth16/src/test/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/BLS12381PairingTest.java
rename to zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java
index 6ed3acb..ff02cd9 100644
--- a/zeroj-verifier-groth16/src/test/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/BLS12381PairingTest.java
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java
@@ -1,6 +1,7 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381;
+package com.bloxbean.cardano.zeroj.bls12381.pairing;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381ProviderTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381ProviderTest.java
new file mode 100644
index 0000000..e1f3c4b
--- /dev/null
+++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/spi/PureJavaBls12381ProviderTest.java
@@ -0,0 +1,107 @@
+package com.bloxbean.cardano.zeroj.bls12381.spi;
+
+import com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G1Point;
+import com.bloxbean.cardano.zeroj.bls12381.ec.G2Point;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp;
+import com.bloxbean.cardano.zeroj.bls12381.field.Fp2;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PureJavaBls12381ProviderTest {
+
+ private final Bls12381Provider provider = Bls12381Providers.pureJava();
+
+ @Test
+ void exposesStandardGenerators() {
+ assertEquals(Bls12381Generators.G1, provider.g1Generator());
+ assertEquals(Bls12381Generators.G2, provider.g2Generator());
+ }
+
+ @Test
+ void exposesGroupOperationsAndCodecs() {
+ assertEquals(G1Point.INFINITY, provider.g1Identity());
+ assertEquals(G2Point.INFINITY, provider.g2Identity());
+ assertEquals(Bls12381Generators.G1.add(Bls12381Generators.G1), provider.g1Add(Bls12381Generators.G1, Bls12381Generators.G1));
+ assertEquals(Bls12381Generators.G2.add(Bls12381Generators.G2), provider.g2Add(Bls12381Generators.G2, Bls12381Generators.G2));
+ assertEquals(Bls12381Generators.G1.negate(), provider.g1Negate(Bls12381Generators.G1));
+ assertEquals(Bls12381Generators.G2.negate(), provider.g2Negate(Bls12381Generators.G2));
+ assertEquals(Bls12381Generators.G1, provider.g1FromCompressed(provider.g1ToCompressed(Bls12381Generators.G1)));
+ assertEquals(Bls12381Generators.G2, provider.g2FromCompressed(provider.g2ToCompressed(Bls12381Generators.G2)));
+ assertEquals(Bls12381Generators.G1, provider.g1FromUncompressed(provider.g1ToUncompressed(Bls12381Generators.G1)));
+ assertEquals(Bls12381Generators.G2, provider.g2FromUncompressed(provider.g2ToUncompressed(Bls12381Generators.G2)));
+ }
+
+ @Test
+ void scalarMul_matchesAffineImplementation() {
+ assertEquals(
+ Bls12381Generators.G1.scalarMul(BigInteger.valueOf(42)),
+ provider.g1ScalarMulGenerator(BigInteger.valueOf(42)));
+ assertEquals(
+ Bls12381Generators.G2.scalarMul(BigInteger.valueOf(42)),
+ provider.g2ScalarMulGenerator(BigInteger.valueOf(42)));
+ }
+
+ @Test
+ void scalarMul_reducesScalarsModuloGroupOrder() {
+ var r = Bls12381Generators.SCALAR_FIELD_ORDER;
+
+ assertEquals(G1Point.INFINITY, provider.g1ScalarMulGenerator(r));
+ assertEquals(G2Point.INFINITY, provider.g2ScalarMulGenerator(r));
+ assertEquals(Bls12381Generators.G1, provider.g1ScalarMulGenerator(r.add(BigInteger.ONE)));
+ assertEquals(Bls12381Generators.G2, provider.g2ScalarMulGenerator(r.add(BigInteger.ONE)));
+ assertEquals(Bls12381Generators.G1.negate(), provider.g1ScalarMulGenerator(BigInteger.valueOf(-1)));
+ assertEquals(Bls12381Generators.G2.negate(), provider.g2ScalarMulGenerator(BigInteger.valueOf(-1)));
+ }
+
+ @Test
+ void secretScalarMul_matchesPublicScalarMul() {
+ var scalar = new BigInteger("12345678901234567890123456789012345678901234567890");
+
+ assertEquals(provider.g1ScalarMulGenerator(scalar), provider.g1SecretScalarMulGenerator(scalar));
+ assertEquals(provider.g2ScalarMulGenerator(scalar), provider.g2SecretScalarMulGenerator(scalar));
+ }
+
+ @Test
+ void rejectsInvalidPoints() {
+ assertThrows(IllegalArgumentException.class,
+ () -> provider.g1ScalarMul(new G1Point(Fp.ZERO, Fp.ZERO), BigInteger.ONE));
+ assertThrows(IllegalArgumentException.class,
+ () -> provider.g2ScalarMul(new G2Point(Fp2.ZERO, Fp2.ZERO), BigInteger.ONE));
+ assertThrows(IllegalArgumentException.class,
+ () -> provider.g1SecretScalarMul(new G1Point(Fp.ZERO, Fp.ZERO), BigInteger.ONE));
+ assertThrows(IllegalArgumentException.class,
+ () -> provider.g2SecretScalarMul(new G2Point(Fp2.ZERO, Fp2.ZERO), BigInteger.ONE));
+ }
+
+ @Test
+ void hashToScalar_isDeterministicAndInRange() {
+ byte[] msg = "abc".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+ byte[] dst = "ZEROJ-BLS12381-SCALAR".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+
+ BigInteger scalar = provider.hashToScalar(msg, dst);
+ assertEquals(scalar, provider.hashToScalar(msg, dst));
+ assertTrue(scalar.signum() >= 0);
+ assertTrue(scalar.compareTo(Bls12381Generators.SCALAR_FIELD_ORDER) < 0);
+ }
+
+ @Test
+ void hashToCurve_returnsValidSubgroupPoints() {
+ byte[] msg = "abc".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+ byte[] g1Dst = "QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+ byte[] g2Dst = "QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+
+ assertTrue(provider.g1HashToCurve(msg, g1Dst).isValid());
+ assertTrue(provider.g2HashToCurve(msg, g2Dst).isValid());
+ }
+
+ @Test
+ void pairingProductIdentityForGeneratorAndNegation() {
+ assertTrue(provider.pairingProductIsIdentity(
+ new G1Point[]{Bls12381Generators.G1, Bls12381Generators.G1.negate()},
+ new G2Point[]{Bls12381Generators.G2, Bls12381Generators.G2}));
+ }
+}
diff --git a/zeroj-blst/README.md b/zeroj-blst/README.md
index 56b25f2..68e86b4 100644
--- a/zeroj-blst/README.md
+++ b/zeroj-blst/README.md
@@ -2,12 +2,13 @@
BLS12-381 cryptographic operations via the [blst](https://github.com/supranational/blst) native library.
-This module wraps the `blst-java` library to provide BLS12-381 pairing operations used by `zeroj-verifier-groth16` for high-performance Groth16 verification. BLS12-381 is the curve used by Cardano's Plutus V3 native BLS primitives.
+This module wraps the `blst-java` library to provide BLS12-381 pairing operations used by `zeroj-verifier-groth16` for high-performance Groth16 verification. It also exposes an explicit `Bls12381Provider` implementation for protocols such as BBS that can opt in to native-backed BLS12-381 operations. BLS12-381 is the curve used by Cardano's Plutus V3 native BLS primitives.
## Key Types
| Type | Description |
|------|-------------|
+| `BlstBls12381Provider` | Explicit native-backed `Bls12381Provider` implementation |
| `BlstPairing` | Wrapper for blst FFM pairing operations (multi-pairing, point validation) |
## Why blst?
@@ -28,3 +29,12 @@ dependencies {
```
Most users don't depend on this module directly — it is pulled in transitively by `zeroj-verifier-groth16`.
+
+Provider selection remains explicit:
+
+```java
+var bls = com.bloxbean.cardano.zeroj.blst.BlstBls12381Provider.createDefault();
+var bbs = com.bloxbean.cardano.zeroj.bbs.BbsService.withBlsProvider(
+ com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite.BLS12381_SHA256,
+ bls);
+```
diff --git a/zeroj-blst/build.gradle b/zeroj-blst/build.gradle
index f18e3a2..cf1ba69 100644
--- a/zeroj-blst/build.gradle
+++ b/zeroj-blst/build.gradle
@@ -6,6 +6,7 @@ description = 'ZeroJ BLS12-381 operations via blst native library (FFM/JNI)'
dependencies {
api project(':zeroj-api')
+ api project(':zeroj-bls12381')
api 'foundation.icon:blst-java:0.3.2'
}
diff --git a/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381Provider.java b/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381Provider.java
new file mode 100644
index 0000000..e85e183
--- /dev/null
+++ b/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381Provider.java
@@ -0,0 +1,171 @@
+package com.bloxbean.cardano.zeroj.blst;
+
+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 com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+import supranational.blst.P1;
+import supranational.blst.P1_Affine;
+import supranational.blst.P2;
+import supranational.blst.P2_Affine;
+import supranational.blst.PT;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * BLS12-381 provider backed by the blst native library.
+ */
+public final class BlstBls12381Provider implements Bls12381Provider {
+ private static final BlstBls12381Provider INSTANCE = new BlstBls12381Provider();
+
+ private BlstBls12381Provider() {}
+
+ public static BlstBls12381Provider createDefault() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String id() {
+ return "zeroj-bls12381-blst";
+ }
+
+ @Override
+ public G1Point g1Generator() {
+ return decodeG1(P1.generator().serialize());
+ }
+
+ @Override
+ public G2Point g2Generator() {
+ return decodeG2(P2.generator().serialize());
+ }
+
+ @Override
+ public G1Point g1Add(G1Point left, G1Point right) {
+ left = Bls12381Codecs.requireValid(left);
+ right = Bls12381Codecs.requireValid(right);
+ if (left.isInfinity()) {
+ return right;
+ }
+ if (right.isInfinity()) {
+ return left;
+ }
+ P1 result = new P1(encodeG1(left));
+ result.add(new P1_Affine(encodeG1(right)));
+ return decodeG1(result.serialize());
+ }
+
+ @Override
+ public G2Point g2Add(G2Point left, G2Point right) {
+ left = Bls12381Codecs.requireValid(left);
+ right = Bls12381Codecs.requireValid(right);
+ if (left.isInfinity()) {
+ return right;
+ }
+ if (right.isInfinity()) {
+ return left;
+ }
+ P2 result = new P2(encodeG2(left));
+ result.add(new P2_Affine(encodeG2(right)));
+ return decodeG2(result.serialize());
+ }
+
+ @Override
+ public G1Point g1Negate(G1Point point) {
+ point = Bls12381Codecs.requireValid(point);
+ if (point.isInfinity()) {
+ return point;
+ }
+ P1 result = new P1(encodeG1(point));
+ result.neg();
+ return decodeG1(result.serialize());
+ }
+
+ @Override
+ public G2Point g2Negate(G2Point point) {
+ point = Bls12381Codecs.requireValid(point);
+ if (point.isInfinity()) {
+ return point;
+ }
+ P2 result = new P2(encodeG2(point));
+ result.neg();
+ return decodeG2(result.serialize());
+ }
+
+ @Override
+ public G1Point g1ScalarMul(G1Point point, BigInteger scalar) {
+ point = Bls12381Codecs.requireValid(point);
+ BigInteger reduced = reduceScalar(scalar);
+ if (point.isInfinity() || reduced.signum() == 0) {
+ return G1Point.INFINITY;
+ }
+ P1 result = new P1(encodeG1(point));
+ result.mult(reduced);
+ return decodeG1(result.serialize());
+ }
+
+ @Override
+ public G2Point g2ScalarMul(G2Point point, BigInteger scalar) {
+ point = Bls12381Codecs.requireValid(point);
+ BigInteger reduced = reduceScalar(scalar);
+ if (point.isInfinity() || reduced.signum() == 0) {
+ return G2Point.INFINITY;
+ }
+ P2 result = new P2(encodeG2(point));
+ result.mult(reduced);
+ return decodeG2(result.serialize());
+ }
+
+ @Override
+ public G1Point g1SecretScalarMul(G1Point point, BigInteger scalar) {
+ return g1ScalarMul(point, scalar);
+ }
+
+ @Override
+ public G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) {
+ return g2ScalarMul(point, scalar);
+ }
+
+ @Override
+ public boolean pairingProductIsIdentity(G1Point[] g1Points, G2Point[] g2Points) {
+ Objects.requireNonNull(g1Points, "g1Points required");
+ Objects.requireNonNull(g2Points, "g2Points required");
+ if (g1Points.length != g2Points.length) {
+ throw new IllegalArgumentException("Pairing point arrays must have the same length");
+ }
+
+ PT accumulated = null;
+ for (int i = 0; i < g1Points.length; i++) {
+ G1Point g1 = Bls12381Codecs.requireValid(g1Points[i]);
+ G2Point g2 = Bls12381Codecs.requireValid(g2Points[i]);
+ if (g1.isInfinity() || g2.isInfinity()) {
+ continue;
+ }
+ PT term = new PT(new P1_Affine(encodeG1(g1)), new P2_Affine(encodeG2(g2)));
+ accumulated = accumulated == null ? term : BlstPairing.mulMlResult(accumulated, term);
+ }
+ return accumulated == null || BlstPairing.finalVerify(accumulated);
+ }
+
+ private static byte[] encodeG1(G1Point point) {
+ return Bls12381Codecs.g1ToUncompressed(point);
+ }
+
+ private static byte[] encodeG2(G2Point point) {
+ return Bls12381Codecs.g2ToUncompressed(point);
+ }
+
+ private static G1Point decodeG1(byte[] bytes) {
+ return Bls12381Codecs.g1FromUncompressed(bytes);
+ }
+
+ private static G2Point decodeG2(byte[] bytes) {
+ return Bls12381Codecs.g2FromUncompressed(bytes);
+ }
+
+ private static BigInteger reduceScalar(BigInteger scalar) {
+ return Objects.requireNonNull(scalar, "scalar required")
+ .mod(Bls12381Generators.SCALAR_FIELD_ORDER);
+ }
+}
diff --git a/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/package-info.java b/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/package-info.java
index 89a20fc..4294a92 100644
--- a/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/package-info.java
+++ b/zeroj-blst/src/main/java/com/bloxbean/cardano/zeroj/blst/package-info.java
@@ -1,4 +1,4 @@
/**
- * BLS12-381 elliptic curve operations via the blst native library.
+ * BLS12-381 provider and pairing operations via the blst native library.
*/
package com.bloxbean.cardano.zeroj.blst;
diff --git a/zeroj-blst/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-blst/reflect-config.json b/zeroj-blst/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-blst/reflect-config.json
index fe51488..1ee4733 100644
--- a/zeroj-blst/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-blst/reflect-config.json
+++ b/zeroj-blst/src/main/resources/META-INF/native-image/com.bloxbean.cardano/zeroj-blst/reflect-config.json
@@ -1 +1,14 @@
-[]
+[
+ {
+ "name": "com.bloxbean.cardano.zeroj.blst.BlstBls12381Provider",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ },
+ {
+ "name": "com.bloxbean.cardano.zeroj.blst.BlstPairing",
+ "allDeclaredConstructors": true,
+ "allDeclaredMethods": true,
+ "allDeclaredFields": true
+ }
+]
diff --git a/zeroj-blst/src/test/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381ProviderTest.java b/zeroj-blst/src/test/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381ProviderTest.java
new file mode 100644
index 0000000..6e005ae
--- /dev/null
+++ b/zeroj-blst/src/test/java/com/bloxbean/cardano/zeroj/blst/BlstBls12381ProviderTest.java
@@ -0,0 +1,100 @@
+package com.bloxbean.cardano.zeroj.blst;
+
+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 com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
+import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Providers;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class BlstBls12381ProviderTest {
+ private static final Bls12381Provider PURE = Bls12381Providers.pureJava();
+ private static final Bls12381Provider BLST = BlstBls12381Provider.createDefault();
+ private static final List SCALARS = List.of(
+ BigInteger.ZERO,
+ BigInteger.ONE,
+ BigInteger.TWO,
+ BigInteger.valueOf(17),
+ Bls12381Generators.SCALAR_FIELD_ORDER.subtract(BigInteger.ONE),
+ Bls12381Generators.SCALAR_FIELD_ORDER,
+ Bls12381Generators.SCALAR_FIELD_ORDER.add(BigInteger.ONE),
+ BigInteger.valueOf(-1));
+
+ @Test
+ void generatorsMatchPureJavaBytes() {
+ assertEquals("zeroj-bls12381-blst", BLST.id());
+ assertArrayEquals(
+ Bls12381Codecs.g1ToUncompressed(PURE.g1Generator()),
+ Bls12381Codecs.g1ToUncompressed(BLST.g1Generator()));
+ assertArrayEquals(
+ Bls12381Codecs.g2ToUncompressed(PURE.g2Generator()),
+ Bls12381Codecs.g2ToUncompressed(BLST.g2Generator()));
+ assertArrayEquals(
+ Bls12381Codecs.g1ToCompressed(PURE.g1Generator()),
+ BLST.g1ToCompressed(BLST.g1Generator()));
+ assertArrayEquals(
+ Bls12381Codecs.g2ToCompressed(PURE.g2Generator()),
+ BLST.g2ToCompressed(BLST.g2Generator()));
+ }
+
+ @Test
+ void g1ArithmeticMatchesPureJava() {
+ G1Point g = BLST.g1Generator();
+ G1Point hashed = BLST.g1HashToCurve(
+ "blst-g1-provider-test".getBytes(StandardCharsets.US_ASCII),
+ "ZEROJ-BLS12381-BLST-G1-TEST".getBytes(StandardCharsets.US_ASCII));
+
+ assertEquals(PURE.g1Add(PURE.g1Generator(), hashed), BLST.g1Add(g, hashed));
+ assertEquals(PURE.g1Negate(hashed), BLST.g1Negate(hashed));
+ assertEquals(hashed, BLST.g1Add(hashed, BLST.g1Identity()));
+ assertEquals(BLST.g1Identity(), BLST.g1Add(hashed, BLST.g1Negate(hashed)));
+ for (BigInteger scalar : SCALARS) {
+ assertEquals(PURE.g1ScalarMul(hashed, scalar), BLST.g1ScalarMul(hashed, scalar), scalar.toString());
+ assertEquals(BLST.g1ScalarMul(hashed, scalar), BLST.g1SecretScalarMul(hashed, scalar), scalar.toString());
+ }
+ }
+
+ @Test
+ void g2ArithmeticMatchesPureJava() {
+ G2Point g = BLST.g2Generator();
+ G2Point hashed = BLST.g2HashToCurve(
+ "blst-g2-provider-test".getBytes(StandardCharsets.US_ASCII),
+ "ZEROJ-BLS12381-BLST-G2-TEST".getBytes(StandardCharsets.US_ASCII));
+
+ assertEquals(PURE.g2Add(PURE.g2Generator(), hashed), BLST.g2Add(g, hashed));
+ assertEquals(PURE.g2Negate(hashed), BLST.g2Negate(hashed));
+ assertEquals(hashed, BLST.g2Add(hashed, BLST.g2Identity()));
+ assertEquals(BLST.g2Identity(), BLST.g2Add(hashed, BLST.g2Negate(hashed)));
+ for (BigInteger scalar : SCALARS) {
+ assertEquals(PURE.g2ScalarMul(hashed, scalar), BLST.g2ScalarMul(hashed, scalar), scalar.toString());
+ assertEquals(BLST.g2ScalarMul(hashed, scalar), BLST.g2SecretScalarMul(hashed, scalar), scalar.toString());
+ }
+ }
+
+ @Test
+ void pairingProductMatchesPureJava() {
+ G1Point g1 = BLST.g1Generator();
+ G2Point g2 = BLST.g2Generator();
+ G1Point negG1 = BLST.g1Negate(g1);
+
+ assertTrue(BLST.pairingProductIsIdentity(new G1Point[]{g1, negG1}, new G2Point[]{g2, g2}));
+ assertFalse(BLST.pairingProductIsIdentity(new G1Point[]{g1}, new G2Point[]{g2}));
+ assertTrue(BLST.pairingProductIsIdentity(new G1Point[]{BLST.g1Identity()}, new G2Point[]{g2}));
+ assertEquals(
+ PURE.pairingProductIsIdentity(new G1Point[]{g1, negG1}, new G2Point[]{g2, g2}),
+ BLST.pairingProductIsIdentity(new G1Point[]{g1, negG1}, new G2Point[]{g2, g2}));
+ }
+
+ @Test
+ void pairingProductRejectsMismatchedArrayLengths() {
+ assertThrows(IllegalArgumentException.class,
+ () -> BLST.pairingProductIsIdentity(new G1Point[]{BLST.g1Generator()}, new G2Point[0]));
+ }
+}
diff --git a/zeroj-bom/build.gradle b/zeroj-bom/build.gradle
index 64b5261..e8335e0 100644
--- a/zeroj-bom/build.gradle
+++ b/zeroj-bom/build.gradle
@@ -19,6 +19,8 @@ dependencies {
api project(':zeroj-verifier-core')
api project(':zeroj-verifier-groth16')
api project(':zeroj-verifier-plonk')
+ api project(':zeroj-bls12381')
+ api project(':zeroj-bls12381-wasm')
api project(':zeroj-blst')
api project(':zeroj-submission')
api project(':zeroj-ingestion')
@@ -29,6 +31,8 @@ dependencies {
api project(':zeroj-circuit-lib')
api project(':zeroj-prover-gnark')
api project(':zeroj-onchain-julc')
+ api project(':zeroj-bbs')
+ api project(':zeroj-bbs-wasm')
// Incubator modules
api project(':zeroj-prover-sidecar')
diff --git a/zeroj-crypto/build.gradle b/zeroj-crypto/build.gradle
index 64f5a5a..e21ee02 100644
--- a/zeroj-crypto/build.gradle
+++ b/zeroj-crypto/build.gradle
@@ -5,6 +5,8 @@ plugins {
description = 'Pure Java optimized cryptographic primitives — Montgomery field arithmetic, EC operations, FFT, MSM'
dependencies {
+ api project(':zeroj-bls12381')
+
// PlonK prover uses the Fiat-Shamir transcript from the verifier module
implementation project(':zeroj-verifier-plonk')
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProofBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProofBLS381.java
index 8c50e5f..f75cac4 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProofBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProofBLS381.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.crypto.groth16;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
/**
* A Groth16 proof for BLS12-381: three group elements (A in G1, B in G2, C in G1).
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProverBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProverBLS381.java
index 0719b84..71fd9c9 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProverBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProverBLS381.java
@@ -1,11 +1,11 @@
package com.bloxbean.cardano.zeroj.crypto.groth16;
import com.bloxbean.cardano.zeroj.api.R1CSConstraint;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.msm.PippengerBLS381;
import com.bloxbean.cardano.zeroj.crypto.poly.FieldFFTBLS381;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProvingKeyBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProvingKeyBLS381.java
index 39413a7..3eac45d 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProvingKeyBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16ProvingKeyBLS381.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.crypto.groth16;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
/**
* Groth16 proving key for BLS12-381.
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/ZkeyImporterBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/ZkeyImporterBLS381.java
index 4702f50..468d74a 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/ZkeyImporterBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/groth16/ZkeyImporterBLS381.java
@@ -1,10 +1,10 @@
package com.bloxbean.cardano.zeroj.crypto.groth16;
import com.bloxbean.cardano.zeroj.api.R1CSConstraint;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp2_381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp2_381;
import java.io.IOException;
import java.io.InputStream;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381.java
index f9b937e..8f29d58 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381.java
@@ -1,8 +1,8 @@
package com.bloxbean.cardano.zeroj.crypto.kzg;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.msm.PippengerBLS381;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381.java
index 06537f3..2aec28d 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.crypto.msm;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProofBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProofBLS381.java
index b215cfb..cca61eb 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProofBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProofBLS381.java
@@ -1,6 +1,6 @@
package com.bloxbean.cardano.zeroj.crypto.plonk;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProverBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProverBLS381.java
index 87c675d..b32fc7c 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProverBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProverBLS381.java
@@ -1,8 +1,8 @@
package com.bloxbean.cardano.zeroj.crypto.plonk;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.kzg.KZGCommitmentBLS381;
import com.bloxbean.cardano.zeroj.crypto.poly.FieldFFTBLS381;
import com.bloxbean.cardano.zeroj.verifier.plonk.FiatShamirTranscript;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProvingKeyBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProvingKeyBLS381.java
index e2e79fd..141c35d 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProvingKeyBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKProvingKeyBLS381.java
@@ -1,8 +1,8 @@
package com.bloxbean.cardano.zeroj.crypto.plonk;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKSetupBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKSetupBLS381.java
index ed81c94..324dd00 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKSetupBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKSetupBLS381.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.crypto.plonk;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.kzg.KZGCommitmentBLS381;
import com.bloxbean.cardano.zeroj.crypto.poly.FieldFFTBLS381;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKZkeyImporterBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKZkeyImporterBLS381.java
index a1cb93c..976aae4 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKZkeyImporterBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKZkeyImporterBLS381.java
@@ -1,10 +1,10 @@
package com.bloxbean.cardano.zeroj.crypto.plonk;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp2_381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp2_381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.poly.FieldFFTBLS381;
import java.io.IOException;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PtauImporterBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PtauImporterBLS381.java
index 19e8d77..a3c9edb 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PtauImporterBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/plonk/PtauImporterBLS381.java
@@ -1,9 +1,9 @@
package com.bloxbean.cardano.zeroj.crypto.plonk;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp2_381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp2_381;
import java.io.IOException;
import java.io.InputStream;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381.java
index 276aabc..1ae9993 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381.java
@@ -1,6 +1,6 @@
package com.bloxbean.cardano.zeroj.crypto.poly;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/Groth16SetupBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/Groth16SetupBLS381.java
index c75a306..77168b2 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/Groth16SetupBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/Groth16SetupBLS381.java
@@ -1,11 +1,11 @@
package com.bloxbean.cardano.zeroj.crypto.setup;
import com.bloxbean.cardano.zeroj.api.R1CSConstraint;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProvingKeyBLS381;
import com.bloxbean.cardano.zeroj.crypto.poly.FieldFFTBLS381;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/PowersOfTauBLS381.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/PowersOfTauBLS381.java
index 52dcccc..24d1eff 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/PowersOfTauBLS381.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/PowersOfTauBLS381.java
@@ -1,10 +1,10 @@
package com.bloxbean.cardano.zeroj.crypto.setup;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.plonk.PtauImporterBLS381;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCache.java b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCache.java
index fe0b0bd..f564d79 100644
--- a/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCache.java
+++ b/zeroj-crypto/src/main/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCache.java
@@ -1,9 +1,9 @@
package com.bloxbean.cardano.zeroj.crypto.setup;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp2_381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFp381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp2_381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFp381;
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProvingKeyBLS381;
import com.bloxbean.cardano.zeroj.crypto.plonk.PtauImporterBLS381;
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381EndToEndTest.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381EndToEndTest.java
index cf288a0..f9961c3 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381EndToEndTest.java
+++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381EndToEndTest.java
@@ -1,11 +1,13 @@
package com.bloxbean.cardano.zeroj.crypto.groth16;
import com.bloxbean.cardano.zeroj.api.R1CSConstraint;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.PowersOfTauBLS381;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381ZkeyEndToEndTest.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381ZkeyEndToEndTest.java
index b90ac63..8fbac2f 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381ZkeyEndToEndTest.java
+++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/groth16/Groth16BLS381ZkeyEndToEndTest.java
@@ -1,9 +1,11 @@
package com.bloxbean.cardano.zeroj.crypto.groth16;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
import com.bloxbean.cardano.zeroj.codec.SnarkjsJsonCodec;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import org.junit.jupiter.api.Test;
import java.io.IOException;
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381Test.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381Test.java
index 5a7030e..4db97a6 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381Test.java
+++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/kzg/KZGCommitmentBLS381Test.java
@@ -1,8 +1,8 @@
package com.bloxbean.cardano.zeroj.crypto.kzg;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.poly.FieldFFTBLS381;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381Test.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381Test.java
index 5c3547b..c743d10 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381Test.java
+++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/msm/PippengerBLS381Test.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.crypto.msm;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKBLS381EndToEndTest.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKBLS381EndToEndTest.java
index 29d0d17..f33c5c8 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKBLS381EndToEndTest.java
+++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/plonk/PlonKBLS381EndToEndTest.java
@@ -2,11 +2,13 @@
import com.bloxbean.cardano.zeroj.api.CurveId;
import com.bloxbean.cardano.zeroj.circuit.CircuitBuilder;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import com.bloxbean.cardano.zeroj.crypto.setup.PowersOfTauBLS381;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import com.bloxbean.cardano.zeroj.verifier.plonk.FiatShamirTranscript;
import org.junit.jupiter.api.Test;
diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381Test.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381Test.java
index b9080bd..dd08c23 100644
--- a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381Test.java
+++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/poly/FieldFFTBLS381Test.java
@@ -1,6 +1,6 @@
package com.bloxbean.cardano.zeroj.crypto.poly;
-import com.bloxbean.cardano.zeroj.crypto.field.MontFr381;
+import com.bloxbean.cardano.zeroj.bls12381.field.MontFr381;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
diff --git a/zeroj-examples/build.gradle b/zeroj-examples/build.gradle
index 578f31e..f4852d0 100644
--- a/zeroj-examples/build.gradle
+++ b/zeroj-examples/build.gradle
@@ -38,6 +38,7 @@ dependencies {
implementation project(':zeroj-ccl')
implementation project(':zeroj-patterns')
implementation project(':zeroj-test-vectors')
+ implementation project(':zeroj-bbs')
// On-chain verifier module (Julc-based Plutus V3 validators)
implementation project(':zeroj-onchain-julc')
diff --git a/zeroj-examples/src/main/java/com/bloxbean/cardano/zeroj/examples/bbs/BbsSelectiveDisclosureExample.java b/zeroj-examples/src/main/java/com/bloxbean/cardano/zeroj/examples/bbs/BbsSelectiveDisclosureExample.java
new file mode 100644
index 0000000..742a2ff
--- /dev/null
+++ b/zeroj-examples/src/main/java/com/bloxbean/cardano/zeroj/examples/bbs/BbsSelectiveDisclosureExample.java
@@ -0,0 +1,74 @@
+package com.bloxbean.cardano.zeroj.examples.bbs;
+
+import com.bloxbean.cardano.zeroj.api.CircuitId;
+import com.bloxbean.cardano.zeroj.api.CurveId;
+import com.bloxbean.cardano.zeroj.api.ProofSystemId;
+import com.bloxbean.cardano.zeroj.api.PublicInputs;
+import com.bloxbean.cardano.zeroj.api.VerificationKeyRef;
+import com.bloxbean.cardano.zeroj.api.VerificationMaterial;
+import com.bloxbean.cardano.zeroj.api.ZkProofEnvelope;
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bbs.BbsPresentation;
+import com.bloxbean.cardano.zeroj.bbs.BbsPresentationCodec;
+import com.bloxbean.cardano.zeroj.bbs.BbsService;
+import com.bloxbean.cardano.zeroj.bbs.BbsSignature;
+import com.bloxbean.cardano.zeroj.bbs.verifier.BbsZkVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Minimal BBS selective-disclosure flow.
+ */
+public final class BbsSelectiveDisclosureExample {
+ private BbsSelectiveDisclosureExample() {}
+
+ public static void main(String[] args) {
+ var service = BbsService.pureJava();
+ var keyPair = service.keyPair(bytes("01234567890123456789012345678901"), bytes("issuer-key-v1"));
+
+ List messages = List.of(
+ bytes("given_name:Alice"),
+ bytes("family_name:Liddell"),
+ bytes("age_over_18:true"),
+ bytes("membership:gold"));
+ byte[] header = bytes("example-credential-v1");
+ byte[] presentationHeader = bytes("verifier-session-123");
+
+ BbsSignature signature = service.sign(keyPair.secretKey(), keyPair.publicKey(), messages, header);
+ boolean signatureValid = service.verify(keyPair.publicKey(), signature, messages, header);
+
+ BbsPresentation presentation = service.derivePresentation(
+ keyPair.publicKey(),
+ signature,
+ messages,
+ header,
+ presentationHeader,
+ new int[]{0, 2});
+ boolean presentationValid = service.verifyPresentation(keyPair.publicKey(), presentation);
+
+ ZkProofEnvelope envelope = ZkProofEnvelope.builder()
+ .proofSystem(ProofSystemId.BBS)
+ .curve(CurveId.BLS12_381)
+ .circuitId(new CircuitId("bbs-selective-disclosure"))
+ .proofBytes(BbsPresentationCodec.encode(presentation))
+ .publicInputs(new PublicInputs(List.of()))
+ .vkRef(new VerificationKeyRef.ById("issuer-key-v1"))
+ .proofFormat(BbsCiphersuite.DEFAULT_PROOF_FORMAT)
+ .build();
+ VerificationMaterial material = VerificationMaterial.of(
+ keyPair.publicKey().bytes(),
+ ProofSystemId.BBS,
+ CurveId.BLS12_381,
+ new CircuitId("bbs-selective-disclosure"));
+ boolean envelopeValid = new BbsZkVerifier(service).verify(envelope, material).proofValid();
+
+ System.out.println("signatureValid=" + signatureValid);
+ System.out.println("presentationValid=" + presentationValid);
+ System.out.println("envelopeValid=" + envelopeValid);
+ }
+
+ private static byte[] bytes(String value) {
+ return value.getBytes(StandardCharsets.UTF_8);
+ }
+}
diff --git a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/auction/SealedBidPureJavaE2ETest.java b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/auction/SealedBidPureJavaE2ETest.java
index 1317fb0..c29dab7 100644
--- a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/auction/SealedBidPureJavaE2ETest.java
+++ b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/auction/SealedBidPureJavaE2ETest.java
@@ -5,7 +5,9 @@
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.PowersOfTauBLS381;
import com.bloxbean.cardano.zeroj.examples.dsl.common.MiMCHash;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
@@ -115,12 +117,12 @@ private boolean verifyOffChain(com.bloxbean.cardano.zeroj.crypto.groth16.Groth16
new G2Point[]{piB, beta, gamma, delta});
}
- private static G1Point toG1(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1 p) {
+ private static G1Point toG1(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1 p) {
if (p.isInfinity()) return G1Point.INFINITY;
return new G1Point(Fp.of(p.xBigInt()), Fp.of(p.yBigInt()));
}
- private static G2Point toG2(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2 p) {
+ private static G2Point toG2(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2 p) {
if (p.isInfinity()) return G2Point.INFINITY;
return new G2Point(
Fp2.of(Fp.of(p.x().reBigInt()), Fp.of(p.x().imBigInt())),
diff --git a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/balance/BalanceThresholdPureJavaE2ETest.java b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/balance/BalanceThresholdPureJavaE2ETest.java
index 9a85a92..03b08da 100644
--- a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/balance/BalanceThresholdPureJavaE2ETest.java
+++ b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/balance/BalanceThresholdPureJavaE2ETest.java
@@ -4,7 +4,9 @@
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProverBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.PowersOfTauBLS381;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
@@ -93,12 +95,12 @@ private boolean verifyOffChain(com.bloxbean.cardano.zeroj.crypto.groth16.Groth16
new G2Point[]{piB, toG2(pk.betaG2()), toG2(setupResult.gammaG2()), toG2(pk.deltaG2())});
}
- private static G1Point toG1(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1 p) {
+ private static G1Point toG1(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1 p) {
if (p.isInfinity()) return G1Point.INFINITY;
return new G1Point(Fp.of(p.xBigInt()), Fp.of(p.yBigInt()));
}
- private static G2Point toG2(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2 p) {
+ private static G2Point toG2(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2 p) {
if (p.isInfinity()) return G2Point.INFINITY;
return new G2Point(
Fp2.of(Fp.of(p.x().reBigInt()), Fp.of(p.x().imBigInt())),
diff --git a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/templates/ParameterizedCircuitE2ETest.java b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/templates/ParameterizedCircuitE2ETest.java
index 18ab938..11a4cdf 100644
--- a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/templates/ParameterizedCircuitE2ETest.java
+++ b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/templates/ParameterizedCircuitE2ETest.java
@@ -7,7 +7,9 @@
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.PowersOfTauBLS381;
import com.bloxbean.cardano.zeroj.examples.dsl.common.MiMCHash;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
@@ -263,12 +265,12 @@ private static int nextPowerOf2Log(int n) {
return p;
}
- private static G1Point toG1(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1 p) {
+ private static G1Point toG1(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1 p) {
if (p.isInfinity()) return G1Point.INFINITY;
return new G1Point(Fp.of(p.xBigInt()), Fp.of(p.yBigInt()));
}
- private static G2Point toG2(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2 p) {
+ private static G2Point toG2(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2 p) {
if (p.isInfinity()) return G2Point.INFINITY;
return new G2Point(
Fp2.of(Fp.of(p.x().reBigInt()), Fp.of(p.x().imBigInt())),
diff --git a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/voting/AnonymousVotingPureJavaE2ETest.java b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/voting/AnonymousVotingPureJavaE2ETest.java
index 3d8df97..f257331 100644
--- a/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/voting/AnonymousVotingPureJavaE2ETest.java
+++ b/zeroj-examples/src/test/java/com/bloxbean/cardano/zeroj/examples/dsl/voting/AnonymousVotingPureJavaE2ETest.java
@@ -5,7 +5,9 @@
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.PowersOfTauBLS381;
import com.bloxbean.cardano.zeroj.examples.dsl.common.MiMCHash;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
@@ -106,12 +108,12 @@ private boolean verifyOffChain(com.bloxbean.cardano.zeroj.crypto.groth16.Groth16
new G2Point[]{toG2(proof.b()), toG2(pk.betaG2()), toG2(setupResult.gammaG2()), toG2(pk.deltaG2())});
}
- private static G1Point toG1(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381.AffineG1 p) {
+ private static G1Point toG1(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381.AffineG1 p) {
if (p.isInfinity()) return G1Point.INFINITY;
return new G1Point(Fp.of(p.xBigInt()), Fp.of(p.yBigInt()));
}
- private static G2Point toG2(com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381.AffineG2 p) {
+ private static G2Point toG2(com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381.AffineG2 p) {
if (p.isInfinity()) return G2Point.INFINITY;
return new G2Point(
Fp2.of(Fp.of(p.x().reBigInt()), Fp.of(p.x().imBigInt())),
diff --git a/zeroj-onchain-julc/build.gradle b/zeroj-onchain-julc/build.gradle
index b9fe4f1..bd37920 100644
--- a/zeroj-onchain-julc/build.gradle
+++ b/zeroj-onchain-julc/build.gradle
@@ -26,8 +26,9 @@ dependencies {
api project(':zeroj-api')
implementation project(':zeroj-blst')
- // Pure Java prover types — used by ProverToCardano to compress prover output
- implementation project(':zeroj-crypto')
+ // Pure Java prover and BLS point types — used by ProverToCardano to compress prover output
+ api project(':zeroj-crypto')
+ api project(':zeroj-bls12381')
// JSON parsing for SnarkjsToCardano
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
diff --git a/zeroj-onchain-julc/src/main/java/com/bloxbean/cardano/zeroj/onchain/julc/ProverToCardano.java b/zeroj-onchain-julc/src/main/java/com/bloxbean/cardano/zeroj/onchain/julc/ProverToCardano.java
index ed14159..38326ce 100644
--- a/zeroj-onchain-julc/src/main/java/com/bloxbean/cardano/zeroj/onchain/julc/ProverToCardano.java
+++ b/zeroj-onchain-julc/src/main/java/com/bloxbean/cardano/zeroj/onchain/julc/ProverToCardano.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.onchain.julc;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProofBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
import supranational.blst.P1_Affine;
diff --git a/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/CircomToOnChainE2ETest.java b/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/CircomToOnChainE2ETest.java
index f6b36d8..d2e6bf5 100644
--- a/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/CircomToOnChainE2ETest.java
+++ b/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/CircomToOnChainE2ETest.java
@@ -1,7 +1,7 @@
package com.bloxbean.cardano.zeroj.onchain.julc;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProverBLS381;
import com.bloxbean.cardano.zeroj.crypto.groth16.ZkeyImporterBLS381;
import com.bloxbean.cardano.julc.core.PlutusData;
diff --git a/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/Groth16BLS12381PureJavaProverTest.java b/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/Groth16BLS12381PureJavaProverTest.java
index 383bcfd..e5682c1 100644
--- a/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/Groth16BLS12381PureJavaProverTest.java
+++ b/zeroj-onchain-julc/src/test/java/com/bloxbean/cardano/zeroj/onchain/julc/Groth16BLS12381PureJavaProverTest.java
@@ -2,8 +2,8 @@
import com.bloxbean.cardano.zeroj.api.CurveId;
import com.bloxbean.cardano.zeroj.circuit.CircuitBuilder;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG1BLS381;
-import com.bloxbean.cardano.zeroj.crypto.ec.JacobianG2BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG1BLS381;
+import com.bloxbean.cardano.zeroj.bls12381.ec.JacobianG2BLS381;
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProverBLS381;
import com.bloxbean.cardano.zeroj.crypto.groth16.Groth16ProofBLS381;
import com.bloxbean.cardano.zeroj.crypto.setup.Groth16SetupBLS381;
diff --git a/zeroj-verifier-groth16/build.gradle b/zeroj-verifier-groth16/build.gradle
index 17ed6be..700e988 100644
--- a/zeroj-verifier-groth16/build.gradle
+++ b/zeroj-verifier-groth16/build.gradle
@@ -7,6 +7,7 @@ description = 'ZeroJ Groth16 verification — BN254 (pure Java) and BLS12-381 (v
dependencies {
api project(':zeroj-backend-spi')
implementation project(':zeroj-codec')
+ implementation project(':zeroj-bls12381')
implementation project(':zeroj-blst')
testImplementation project(':zeroj-test-vectors')
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/Groth16BLS12381PureJavaVerifier.java b/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/Groth16BLS12381PureJavaVerifier.java
index d5c0e0f..d0471b5 100644
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/Groth16BLS12381PureJavaVerifier.java
+++ b/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/Groth16BLS12381PureJavaVerifier.java
@@ -4,7 +4,9 @@
import com.bloxbean.cardano.zeroj.backend.spi.BackendDescriptor;
import com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier;
import com.bloxbean.cardano.zeroj.codec.SnarkjsJsonCodec;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import java.math.BigInteger;
import java.util.List;
@@ -13,7 +15,7 @@
* Pure Java Groth16 verifier for BLS12-381 — no native dependencies.
*
* Uses the pure Java BLS12-381 field arithmetic and pairing from
- * {@link com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field}.
+ * {@link com.bloxbean.cardano.zeroj.bls12381}.
*
* Verification equation: e(A, B) * e(-alpha, beta) * e(-vk_x, gamma) * e(-C, delta) == 1
*
diff --git a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G2Point.java b/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G2Point.java
deleted file mode 100644
index 8c20311..0000000
--- a/zeroj-verifier-groth16/src/main/java/com/bloxbean/cardano/zeroj/verifier/groth16/bls12381/field/G2Point.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field;
-
-import java.math.BigInteger;
-
-/**
- * BLS12-381 G2 point in affine coordinates on the twisted curve y^2 = x^3 + 4(1+u) over Fp2.
- */
-public record G2Point(Fp2 x, Fp2 y) {
-
- public static final G2Point INFINITY = new G2Point(null, null);
-
- public boolean isInfinity() { return x == null; }
-
- /** Create from snarkjs projective [[x_c0,x_c1],[y_c0,y_c1],[z_c0,z_c1]]. */
- public static G2Point fromProjective(BigInteger xc0, BigInteger xc1,
- BigInteger yc0, BigInteger yc1,
- BigInteger zc0, BigInteger zc1) {
- var z = Fp2.of(Fp.of(zc0), Fp.of(zc1));
- if (z.isZero()) return INFINITY;
- if (z.c0().equals(Fp.ONE) && z.c1().isZero()) {
- return new G2Point(Fp2.of(Fp.of(xc0), Fp.of(xc1)), Fp2.of(Fp.of(yc0), Fp.of(yc1)));
- }
- var zInv = z.inv();
- return new G2Point(
- Fp2.of(Fp.of(xc0), Fp.of(xc1)).mul(zInv),
- Fp2.of(Fp.of(yc0), Fp.of(yc1)).mul(zInv));
- }
-
- public G2Point negate() {
- return isInfinity() ? this : new G2Point(x, y.neg());
- }
-
- public G2Point add(G2Point o) {
- if (this.isInfinity()) return o;
- if (o.isInfinity()) return this;
- if (this.x.equals(o.x)) {
- return this.y.equals(o.y) ? this.doublePoint() : INFINITY;
- }
- var lambda = o.y.sub(this.y).mul(o.x.sub(this.x).inv());
- var x3 = lambda.square().sub(this.x).sub(o.x);
- var y3 = lambda.mul(this.x.sub(x3)).sub(this.y);
- return new G2Point(x3, y3);
- }
-
- public G2Point doublePoint() {
- if (isInfinity() || y.isZero()) return INFINITY;
- var three = Fp2.of(Fp.of(3), Fp.ZERO);
- var lambda = x.square().mul(three).mul(y.add(y).inv());
- var x3 = lambda.square().sub(x).sub(x);
- var y3 = lambda.mul(x.sub(x3)).sub(y);
- return new G2Point(x3, y3);
- }
-}
diff --git a/zeroj-verifier-plonk/build.gradle b/zeroj-verifier-plonk/build.gradle
index 287e36f..c9cdef2 100644
--- a/zeroj-verifier-plonk/build.gradle
+++ b/zeroj-verifier-plonk/build.gradle
@@ -7,6 +7,7 @@ description = 'ZeroJ PlonK verification — pure Java for BLS12-381 and BN254, n
dependencies {
api project(':zeroj-backend-spi')
implementation project(':zeroj-codec')
+ implementation project(':zeroj-bls12381')
implementation project(':zeroj-verifier-groth16') // BN254 field arithmetic reuse
testImplementation project(':zeroj-test-vectors')
diff --git a/zeroj-verifier-plonk/src/main/java/com/bloxbean/cardano/zeroj/verifier/plonk/PlonkBLS12381Verifier.java b/zeroj-verifier-plonk/src/main/java/com/bloxbean/cardano/zeroj/verifier/plonk/PlonkBLS12381Verifier.java
index 5f73a92..8d1359a 100644
--- a/zeroj-verifier-plonk/src/main/java/com/bloxbean/cardano/zeroj/verifier/plonk/PlonkBLS12381Verifier.java
+++ b/zeroj-verifier-plonk/src/main/java/com/bloxbean/cardano/zeroj/verifier/plonk/PlonkBLS12381Verifier.java
@@ -3,7 +3,9 @@
import com.bloxbean.cardano.zeroj.api.*;
import com.bloxbean.cardano.zeroj.backend.spi.BackendDescriptor;
import com.bloxbean.cardano.zeroj.backend.spi.ZkVerifier;
-import com.bloxbean.cardano.zeroj.verifier.groth16.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.ec.*;
+import com.bloxbean.cardano.zeroj.bls12381.field.*;
+import com.bloxbean.cardano.zeroj.bls12381.pairing.BLS12381Pairing;
import java.math.BigInteger;
import java.util.List;
From 1b1fa948c4d1f8aff8c8fc9746289f286f79fa8a Mon Sep 17 00:00:00 2001
From: Satya
Date: Mon, 11 May 2026 12:28:31 +0800
Subject: [PATCH 2/6] refactor(bbs): reserve zeroj-bbs-wasm for full Rust-WASM
BBS only
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
...9-cfrg-bbs-pure-java-and-wasm-providers.md | 83 +++++++----
zeroj-bbs-wasm/README.md | 56 +++++++-
zeroj-bbs-wasm/build.gradle | 5 +-
.../zeroj/bbs/wasm/WasmBbsProvider.java | 132 ------------------
.../zeroj/bbs/wasm/WasmBbsProviderTest.java | 82 -----------
5 files changed, 106 insertions(+), 252 deletions(-)
delete mode 100644 zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
delete mode 100644 zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
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
index 3208ebe..ae2bfdc 100644
--- a/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
+++ b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
@@ -151,31 +151,43 @@ 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. Add explicit optional non-default providers
+### 6. BLS-provider swapping happens in `zeroj-bbs`, not in separate modules
-`zeroj-bbs-wasm` implements the same `BbsProvider` SPI as an explicit opt-in.
-The first implementation reuses the vector-compatible Java BBS core and swaps
-the BLS12-381 primitive provider to `zeroj-bls12381-wasm`, which is Rust
-compiled to `wasm32-unknown-unknown` and executed through Chicory 1.7.5.
-
-This keeps one audited BBS algorithm path while still allowing WASM-backed
-pairing and scalar multiplication. A future optimization may add a coarse Rust
-BBS ABI if vector compatibility and performance justify the extra implementation
-surface.
-
-Native `blst` is exposed as a BLS12-381 provider from `zeroj-blst`. BBS can use
-it through explicit BLS provider selection without adding a separate
-`zeroj-bbs-blst` module:
+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 test or user-selected optional
-dependencies.
+`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.
-If a coarse Rust BBS ABI is added later, it should use operations like:
+### 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`
@@ -184,13 +196,16 @@ If a coarse Rust BBS ABI is added later, it should use operations like:
- `zeroj_bbs_proof_gen`
- `zeroj_bbs_proof_verify`
-The Rust candidate may be `zkryptium` if it compiles cleanly to
-`wasm32-unknown-unknown`, has no unexpected host imports, and passes the pinned
-draft-10 vectors. 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 into a draft-10 shape.
+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 should mirror the hardened `zeroj-bls12381-wasm` pattern:
+The WASM ABI must mirror the hardened `zeroj-bls12381-wasm` pattern:
- committed `Cargo.lock`
- pinned `rust-toolchain.toml`
@@ -199,8 +214,15 @@ The WASM ABI should mirror the hardened `zeroj-bls12381-wasm` pattern:
- no unexpected host imports
- 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
+
+`zeroj_bbs_proof_gen` must accept caller-supplied random scalars so that the
+CFRG mocked-RNG draft-10 proof vectors reproduce byte-exactly. Production
+callers may omit the random scalars; the Rust crate then uses its internal
+deterministic-from-seed RNG. Host RNG is not permitted.
-### 7. ZeroJ verifier integration
+### 8. ZeroJ verifier integration
`zeroj-bbs` should provide a BBS verifier for ZeroJ's verifier APIs:
@@ -318,10 +340,13 @@ pure Java to WASM.
7. Implement `ProofGen` and `ProofVerify`; pass proof vectors and negative
tests.
8. Add `BbsService`, presentation wrappers, and ZeroJ verifier integration.
-9. Create `zeroj-bbs-wasm` with a Rust candidate gated by draft vectors, no
- host imports, and hardened memory cleanup tests.
-10. Run the shared conformance suite against pure Java, WASM, and blst BLS
- providers.
+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
@@ -333,6 +358,7 @@ pure Java to WASM.
| 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 a `getrandom`/`wasm-bindgen` host shim | Medium | Reject candidates that need host RNG; require caller-supplied random scalars for the proof generation ABI |
| Users confuse CFRG core with W3C Data Integrity packaging | Medium | Keep proof format names explicit and document policy/package boundaries |
## References
@@ -343,3 +369,4 @@ pure Java to WASM.
- 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/zeroj-bbs-wasm/README.md b/zeroj-bbs-wasm/README.md
index ad5df25..3575a36 100644
--- a/zeroj-bbs-wasm/README.md
+++ b/zeroj-bbs-wasm/README.md
@@ -1,16 +1,58 @@
# zeroj-bbs-wasm
-Optional CFRG BBS provider backed by `zeroj-bls12381-wasm`.
+Reserved for the **full Rust-WASM** CFRG BBS provider, per ADR-0019 §7.
-The BBS draft-10 algorithm is shared with `zeroj-bbs`; this module swaps the
-BLS12-381 primitive provider to the Rust `bls12_381` WASM backend executed by
-Chicory. Provider selection is explicit:
+The Rust BBS algorithm is compiled to `wasm32-unknown-unknown` and executed
+through Chicory. Unlike the hybrid path (Java BBS + WASM BLS primitives), the
+entire BBS algorithm runs inside WebAssembly, eliminating per-pairing and
+per-scalar-mul cross-boundary calls.
+
+> **Status:** scaffolding only. Source code is not yet committed pending the
+> `zkryptium` POC gates (compiles to `wasm32-unknown-unknown`, no unexpected
+> host imports, passes pinned CFRG draft-10 vectors byte-for-byte). See
+> ADR-0019 §7 for the gate criteria.
+
+## If you only want WASM-backed BLS primitives, you do not need this module
+
+Hybrid mode — Java BBS algorithm with WASM BLS pairings and scalar muls — is
+reachable directly from `zeroj-bbs`:
+
+```java
+import com.bloxbean.cardano.zeroj.bbs.BbsService;
+import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
+import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
+
+var service = BbsService.withBlsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ WasmBls12381Provider.createDefault());
+```
+
+The same pattern works for the native `blst` BLS provider from `zeroj-blst`:
+
+```java
+import com.bloxbean.cardano.zeroj.blst.BlstBls12381Provider;
+
+var service = BbsService.withBlsProvider(
+ BbsCiphersuite.BLS12381_SHA256,
+ BlstBls12381Provider.createDefault());
+```
+
+`zeroj-bbs/src/test/java/com/bloxbean/cardano/zeroj/bbs/BbsBlsProviderConformanceTest`
+already runs the full CFRG draft-10 fixture suite against all three BLS
+providers via this `withBlsProvider` entry point.
+
+## What `zeroj-bbs-wasm` will be for
+
+When ready, this module will expose a `WasmBbsProvider` whose `keyGen`,
+`skToPk`, `sign`, `verify`, `proofGen`, and `proofVerify` methods are all
+single WASM calls dispatching to coarse `zeroj_bbs_*` ABI exports. Production
+callers opt in:
```java
var provider = com.bloxbean.cardano.zeroj.bbs.wasm.WasmBbsProvider.createDefault();
var service = new com.bloxbean.cardano.zeroj.bbs.BbsService(provider);
```
-The generated WASM artifact comes from `zeroj-bls12381-wasm` during the Gradle
-build. Install Rust with the pinned toolchain and the `wasm32-unknown-unknown`
-target before running this module's tests.
+The generated WASM artifact is built from this module's own `rust/` crate
+during the Gradle build; install Rust with the pinned toolchain and the
+`wasm32-unknown-unknown` target before running the module's tests.
diff --git a/zeroj-bbs-wasm/build.gradle b/zeroj-bbs-wasm/build.gradle
index 09cc3e3..113532b 100644
--- a/zeroj-bbs-wasm/build.gradle
+++ b/zeroj-bbs-wasm/build.gradle
@@ -2,11 +2,10 @@ plugins {
id 'java-library'
}
-description = 'ZeroJ BBS WASM-backed provider using BLS12-381 Rust WASM primitives'
+description = 'ZeroJ full Rust-WASM CFRG BBS provider (reserved; implementation pending zkryptium POC gates per ADR-0019 §7)'
dependencies {
api project(':zeroj-bbs')
- implementation project(':zeroj-bls12381-wasm')
}
publishing {
@@ -14,7 +13,7 @@ publishing {
mavenJava(MavenPublication) {
pom {
name = 'ZeroJ BBS WASM'
- description = 'Optional CFRG BBS provider using Rust WASM BLS12-381 primitives and Chicory'
+ description = 'Optional CFRG BBS provider with the entire BBS algorithm running inside WebAssembly via Chicory'
}
}
}
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
deleted file mode 100644
index 6b3994c..0000000
--- a/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
+++ /dev/null
@@ -1,132 +0,0 @@
-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.CfrgBbsCore;
-import com.bloxbean.cardano.zeroj.bbs.spi.BbsProvider;
-import com.bloxbean.cardano.zeroj.bls12381.spi.Bls12381Provider;
-import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
-
-import java.security.SecureRandom;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * CFRG BBS provider backed by the ZeroJ BLS12-381 Rust WASM provider.
- */
-public final class WasmBbsProvider implements BbsProvider {
- private final BbsCiphersuite ciphersuite;
- private final Bls12381Provider bls;
-
- public WasmBbsProvider(BbsCiphersuite ciphersuite, Bls12381Provider bls) {
- this.ciphersuite = Objects.requireNonNull(ciphersuite, "ciphersuite required");
- this.bls = Objects.requireNonNull(bls, "BLS provider required");
- }
-
- public static WasmBbsProvider createDefault() {
- return new WasmBbsProvider(BbsCiphersuite.BLS12381_SHA256, WasmBls12381Provider.createDefault());
- }
-
- @Override
- public String id() {
- return "zeroj-bbs-wasm-bls12381-zkcrypto";
- }
-
- @Override
- public BbsCiphersuite ciphersuite() {
- return ciphersuite;
- }
-
- @Override
- public BbsSecretKey keyGen(byte[] keyMaterial, byte[] keyInfo) {
- return new BbsSecretKey(CfrgBbsCore.keyGen(ciphersuite, keyMaterial, keyInfo), ciphersuite);
- }
-
- @Override
- public BbsPublicKey skToPk(BbsSecretKey secretKey) {
- requireSuite(secretKey);
- return new BbsPublicKey(CfrgBbsCore.skToPk(secretKey.value(), bls), ciphersuite);
- }
-
- @Override
- public BbsSignature sign(BbsSecretKey secretKey, BbsPublicKey publicKey, List messages, byte[] header) {
- requireSuite(secretKey);
- requireSuite(publicKey);
- return new BbsSignature(CfrgBbsCore.sign(
- secretKey.value(), publicKey.bytes(), messages, header, ciphersuite, bls), ciphersuite);
- }
-
- @Override
- public boolean verify(BbsPublicKey publicKey, BbsSignature signature, List messages, byte[] header) {
- if (publicKey.ciphersuite() != ciphersuite || signature.ciphersuite() != ciphersuite) {
- return false;
- }
- return CfrgBbsCore.verify(publicKey.bytes(), signature.bytes(), messages, header, ciphersuite, bls);
- }
-
- @Override
- public BbsProof proofGen(
- BbsPublicKey publicKey,
- BbsSignature signature,
- List messages,
- byte[] header,
- byte[] presentationHeader,
- int[] disclosedIndexes,
- SecureRandom random) {
- requireSuite(publicKey);
- requireSuite(signature);
- return new BbsProof(CfrgBbsCore.proofGen(
- publicKey.bytes(),
- signature.bytes(),
- messages,
- header,
- presentationHeader,
- disclosedIndexes,
- ciphersuite,
- bls,
- random), 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 CfrgBbsCore.proofVerify(
- publicKey.bytes(),
- proof.bytes(),
- header,
- presentationHeader,
- disclosedMessages,
- disclosedIndexes,
- ciphersuite,
- bls);
- }
-
- 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/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
deleted file mode 100644
index db4c48c..0000000
--- a/zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.bloxbean.cardano.zeroj.bbs.wasm;
-
-import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
-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.CfrgBbsCore;
-import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
-import org.junit.jupiter.api.Test;
-
-import java.math.BigInteger;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class WasmBbsProviderTest {
- private static final BbsCiphersuite SUITE = BbsCiphersuite.BLS12381_SHA256;
- private static final byte[] KEY_MATERIAL = hex("746869732d49532d6a7573742d616e2d546573742d494b4d2d746f2d67656e65726174652d246528724074232d6b6579");
- private static final byte[] KEY_INFO = hex("746869732d49532d736f6d652d6b65792d6d657461646174612d746f2d62652d757365642d696e2d746573742d6b65792d67656e");
- private static final byte[] SK = hex("60e55110f76883a13d030b2f6bd11883422d5abde717569fc0731f51237169fc");
- private static final byte[] PK = hex("a820f230f6ae38503b86c70dc50b61c58a77e45c39ab25c0652bbaa8fa136f2851bd4781c9dcde39fc9d1d52c9e60268061e7d7632171d91aa8d460acee0e96f1e7c4cfb12d3ff9ab5d5dc91c277db75c845d649ef3c4f63aebc364cd55ded0c");
- private static final byte[] HEADER = hex("11223344556677889900aabbccddeeff");
- private static final byte[] PRESENTATION_HEADER = hex("bed231d880675ed101ead304512e043ade9958dd0241ea70b4b3957fba941501");
- private static final byte[] MULTI_SIGNATURE = hex("8339b285a4acd89dec7777c09543a43e3cc60684b0a6f8ab335da4825c96e1463e28f8c5f4fd0641d19cec5920d3a8ff4bedb6c9691454597bbd298288abed3632078557b2ace7d44caed846e1a0a1e8");
- private static final byte[] SOME_DISCLOSED_PROOF = hex("a2ed608e8e12ed21abc2bf154e462d744a367c7f1f969bdbf784a2a134c7db2d340394223a5397a3011b1c340ebc415199462ba6f31106d8a6da8b513b37a47afe93c9b3474d0d7a354b2edc1b88818b063332df774c141f7a07c48fe50d452f897739228c88afc797916dca01e8f03bd9c5375c7a7c59996e514bb952a436afd24457658acbaba5ddac2e693ac481356918cd38025d86b28650e909defe9604a7259f44386b861608be742af7775a2e71a6070e5836f5f54dc43c60096834a5b6da295bf8f081f72b7cdf7f3b4347fb3ff19edaa9e74055c8ba46dbcb7594fb2b06633bb5324192eb9be91be0d33e453b4d3127459de59a5e2193c900816f049a02cb9127dac894418105fa1641d5a206ec9c42177af9316f433417441478276ca0303da8f941bf2e0222a43251cf5c2bf6eac1961890aa740534e519c1767e1223392a3a286b0f4d91f7f25217a7862b8fcc1810cdcfddde2a01c80fcc90b632585fec12dc4ae8fea1918e9ddeb9414623a457e88f53f545841f9d5dcb1f8e160d1560770aa79d65e2eca8edeaecb73fb7e995608b820c4a64de6313a370ba05dc25ed7c1d185192084963652f2870341bdaa4b1a37f8c06348f38a4f80c5a2650a21d59f09e8305dcd3fc3ac30e2a");
-
- @Test
- void providerMatchesDraft10KeyAndSignatureVectors() {
- var provider = WasmBbsProvider.createDefault();
- BbsSecretKey secretKey = provider.keyGen(KEY_MATERIAL, KEY_INFO);
- BbsPublicKey publicKey = provider.skToPk(secretKey);
-
- assertArrayEquals(SK, secretKey.toBytes());
- assertArrayEquals(PK, publicKey.bytes());
-
- BbsSignature signature = provider.sign(secretKey, publicKey, messages(), HEADER);
- assertArrayEquals(MULTI_SIGNATURE, signature.bytes());
- assertTrue(provider.verify(publicKey, signature, messages(), HEADER));
- }
-
- @Test
- void wasmBlsProviderVerifiesOfficialSomeDisclosedProofVector() {
- var bls = WasmBls12381Provider.createDefault();
- int[] disclosedIndexes = {0, 2, 4, 6};
- List disclosedMessages = Arrays.stream(disclosedIndexes)
- .mapToObj(messages()::get)
- .toList();
-
- assertTrue(CfrgBbsCore.proofVerify(
- PK,
- SOME_DISCLOSED_PROOF,
- HEADER,
- PRESENTATION_HEADER,
- disclosedMessages,
- disclosedIndexes,
- SUITE,
- bls));
- }
-
- private static List messages() {
- return List.of(
- hex("9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"),
- hex("c344136d9ab02da4dd5908bbba913ae6f58c2cc844b802a6f811f5fb075f9b80"),
- hex("7372e9daa5ed31e6cd5c825eac1b855e84476a1d94932aa348e07b73"),
- hex("77fe97eb97a1ebe2e81e4e3597a3ee740a66e9ef2412472c"),
- hex("496694774c5604ab1b2544eababcf0f53278ff50"),
- hex("515ae153e22aae04ad16f759e07237b4"),
- hex("d183ddc6e2665aa4e2f088af"),
- hex("ac55fb33a75909ed"),
- hex("96012096"),
- new byte[0]);
- }
-
- 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;
- }
-}
From 268b7d38ee4ac5d0c813db782ac35664e1a444f2 Mon Sep 17 00:00:00 2001
From: Satya
Date: Mon, 11 May 2026 21:27:58 +0800
Subject: [PATCH 3/6] feat(bbs-wasm): full Rust-WASM CFRG BBS provider via
zkryptium + Chicory
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
.gitignore | 1 +
...9-cfrg-bbs-pure-java-and-wasm-providers.md | 27 +-
zeroj-bbs-wasm/README.md | 118 +++--
zeroj-bbs-wasm/build.gradle | 37 +-
zeroj-bbs-wasm/rust/Cargo.lock | 475 ++++++++++++++++++
zeroj-bbs-wasm/rust/Cargo.toml | 20 +
zeroj-bbs-wasm/rust/rust-toolchain.toml | 4 +
zeroj-bbs-wasm/rust/src/lib.rs | 453 +++++++++++++++++
.../zeroj/bbs/wasm/Bbs12381WasmClient.java | 373 ++++++++++++++
.../zeroj/bbs/wasm/Bbs12381WasmException.java | 14 +
.../zeroj/bbs/wasm/WasmBbsProvider.java | 153 ++++++
.../zeroj-bbs-wasm/resource-config.json | 9 +
.../zeroj/bbs/wasm/WasmBbsProviderTest.java | 341 +++++++++++++
13 files changed, 1984 insertions(+), 41 deletions(-)
create mode 100644 zeroj-bbs-wasm/rust/Cargo.lock
create mode 100644 zeroj-bbs-wasm/rust/Cargo.toml
create mode 100644 zeroj-bbs-wasm/rust/rust-toolchain.toml
create mode 100644 zeroj-bbs-wasm/rust/src/lib.rs
create mode 100644 zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmClient.java
create mode 100644 zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmException.java
create mode 100644 zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
create mode 100644 zeroj-bbs-wasm/src/main/resources/META-INF/native-image/com.bloxbean.cardano.zeroj/zeroj-bbs-wasm/resource-config.json
create mode 100644 zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
diff --git a/.gitignore b/.gitignore
index 6dde657..5b0ca67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ incubator/zeroj-prover-rapidsnark/src/main/resources/native/*/librapidsnark.*
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/0019-cfrg-bbs-pure-java-and-wasm-providers.md b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
index ae2bfdc..7892b13 100644
--- a/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
+++ b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
@@ -211,16 +211,33 @@ The WASM ABI must mirror the hardened `zeroj-bls12381-wasm` pattern:
- pinned `rust-toolchain.toml`
- generated `.wasm` built by Gradle, not checked in
- exported memory plus explicit `alloc` and `dealloc`
-- no unexpected host imports
- 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
-`zeroj_bbs_proof_gen` must accept caller-supplied random scalars so that the
-CFRG mocked-RNG draft-10 proof vectors reproduce byte-exactly. Production
-callers may omit the random scalars; the Rust crate then uses its internal
-deterministic-from-seed RNG. Host RNG is not permitted.
+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
diff --git a/zeroj-bbs-wasm/README.md b/zeroj-bbs-wasm/README.md
index 3575a36..16841a8 100644
--- a/zeroj-bbs-wasm/README.md
+++ b/zeroj-bbs-wasm/README.md
@@ -1,58 +1,108 @@
# zeroj-bbs-wasm
-Reserved for the **full Rust-WASM** CFRG BBS provider, per ADR-0019 §7.
+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.
-The Rust BBS algorithm is compiled to `wasm32-unknown-unknown` and executed
-through Chicory. Unlike the hybrid path (Java BBS + WASM BLS primitives), the
-entire BBS algorithm runs inside WebAssembly, eliminating per-pairing and
-per-scalar-mul cross-boundary calls.
+See [ADR-0019](../docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md) §7
+for the design rationale.
-> **Status:** scaffolding only. Source code is not yet committed pending the
-> `zkryptium` POC gates (compiles to `wasm32-unknown-unknown`, no unexpected
-> host imports, passes pinned CFRG draft-10 vectors byte-for-byte). See
-> ADR-0019 §7 for the gate criteria.
+## When to use this module
-## If you only want WASM-backed BLS primitives, you do not need this module
-
-Hybrid mode — Java BBS algorithm with WASM BLS pairings and scalar muls — is
-reachable directly from `zeroj-bbs`:
+Use the full Rust-WASM provider when you want maximum throughput for BBS
+operations and are OK with shipping a small WebAssembly artifact. The
+algorithm runs end-to-end inside WASM, so signing, verifying, and proof
+generation incur exactly one cross-boundary call per operation.
```java
-import com.bloxbean.cardano.zeroj.bbs.BbsService;
import com.bloxbean.cardano.zeroj.bbs.BbsCiphersuite;
-import com.bloxbean.cardano.zeroj.bls12381.wasm.WasmBls12381Provider;
+import com.bloxbean.cardano.zeroj.bbs.BbsService;
+import com.bloxbean.cardano.zeroj.bbs.wasm.WasmBbsProvider;
-var service = BbsService.withBlsProvider(
- BbsCiphersuite.BLS12381_SHA256,
- WasmBls12381Provider.createDefault());
+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);
```
-The same pattern works for the native `blst` BLS provider from `zeroj-blst`:
+## 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
-import com.bloxbean.cardano.zeroj.blst.BlstBls12381Provider;
+// 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/com/bloxbean/cardano/zeroj/bbs/BbsBlsProviderConformanceTest`
-already runs the full CFRG draft-10 fixture suite against all three BLS
-providers via this `withBlsProvider` entry point.
+`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.
-## What `zeroj-bbs-wasm` will be for
+## ABI
-When ready, this module will expose a `WasmBbsProvider` whose `keyGen`,
-`skToPk`, `sign`, `verify`, `proofGen`, and `proofVerify` methods are all
-single WASM calls dispatching to coarse `zeroj_bbs_*` ABI exports. Production
-callers opt in:
+The WASM module exposes:
-```java
-var provider = com.bloxbean.cardano.zeroj.bbs.wasm.WasmBbsProvider.createDefault();
-var service = new com.bloxbean.cardano.zeroj.bbs.BbsService(provider);
+- `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.
+
+## 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
```
-The generated WASM artifact is built from this module's own `rust/` crate
-during the Gradle build; install Rust with the pinned toolchain and the
-`wasm32-unknown-unknown` target before running the module's tests.
+## 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
index 113532b..3e404e2 100644
--- a/zeroj-bbs-wasm/build.gradle
+++ b/zeroj-bbs-wasm/build.gradle
@@ -2,10 +2,43 @@ plugins {
id 'java-library'
}
-description = 'ZeroJ full Rust-WASM CFRG BBS provider (reserved; implementation pending zkryptium POC gates per ADR-0019 §7)'
+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 {
@@ -13,7 +46,7 @@ publishing {
mavenJava(MavenPublication) {
pom {
name = 'ZeroJ BBS WASM'
- description = 'Optional CFRG BBS provider with the entire BBS algorithm running inside WebAssembly via Chicory'
+ 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..c204722
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/Bbs12381WasmClient.java
@@ -0,0 +1,373 @@
+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;
+
+ static final byte SUITE_SHA256 = 0;
+ static final byte SUITE_SHAKE256 = 1;
+
+ private final Instance instance;
+ private final Memory memory;
+ private final SecureRandom random;
+
+ 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.random = random;
+ try {
+ 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];
+ this.random.nextBytes(buf);
+ inst.memory().write(ptr, buf);
+ return new long[]{0L};
+ });
+ ImportValues imports = ImportValues.builder().addFunction(hostGetrandom).build();
+ this.instance = Instance.builder(Parser.parse(wasmBytes))
+ .withImportValues(imports)
+ .build();
+ 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);
+ }
+ }
+
+ 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");
+ Objects.requireNonNull(keyMaterial, "keyMaterial required");
+ Objects.requireNonNull(keyInfo, "keyInfo required");
+ ByteBuffer req = ByteBuffer.allocate(1 + 4 + keyMaterial.length + 4 + keyInfo.length)
+ .order(ByteOrder.LITTLE_ENDIAN);
+ 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");
+ Objects.requireNonNull(pk, "pk required");
+ Objects.requireNonNull(header, "header required");
+ Objects.requireNonNull(messages, "messages required");
+ if (sk.length != 32) {
+ throw new IllegalArgumentException("BBS secret key must be 32 bytes, got " + sk.length);
+ }
+ if (pk.length != 96) {
+ throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
+ }
+ ByteBuffer req = ByteBuffer.allocate(signRequestLen(header, messages)).order(ByteOrder.LITTLE_ENDIAN);
+ 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");
+ Objects.requireNonNull(pk, "pk required");
+ Objects.requireNonNull(signature, "signature required");
+ Objects.requireNonNull(header, "header required");
+ Objects.requireNonNull(messages, "messages required");
+ if (pk.length != 96) {
+ throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
+ }
+ if (signature.length != 80) {
+ throw new IllegalArgumentException("BBS signature must be 80 bytes, got " + signature.length);
+ }
+ ByteBuffer req = ByteBuffer.allocate(1 + 96 + 80 + 4 + header.length + 4 + messagesPayload(messages))
+ .order(ByteOrder.LITTLE_ENDIAN);
+ 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) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(pk, "pk required");
+ Objects.requireNonNull(signature, "signature required");
+ Objects.requireNonNull(header, "header required");
+ Objects.requireNonNull(presentationHeader, "presentationHeader required");
+ Objects.requireNonNull(messages, "messages required");
+ Objects.requireNonNull(disclosedIndexes, "disclosedIndexes required");
+ if (pk.length != 96) {
+ throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
+ }
+ if (signature.length != 80) {
+ throw new IllegalArgumentException("BBS signature must be 80 bytes, got " + signature.length);
+ }
+ int size = 1 + 96 + 80 + 4 + header.length + 4 + presentationHeader.length
+ + 4 + messagesPayload(messages) + 4 + 4 * disclosedIndexes.length;
+ ByteBuffer req = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ 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 invoke("zeroj_bbs_proof_gen", req.array());
+ }
+
+ public boolean proofVerify(
+ BbsCiphersuite ciphersuite,
+ byte[] pk,
+ byte[] proof,
+ byte[] header,
+ byte[] presentationHeader,
+ List disclosedMessages,
+ int[] disclosedIndexes) {
+ Objects.requireNonNull(ciphersuite, "ciphersuite required");
+ Objects.requireNonNull(pk, "pk required");
+ Objects.requireNonNull(proof, "proof required");
+ Objects.requireNonNull(header, "header required");
+ Objects.requireNonNull(presentationHeader, "presentationHeader required");
+ Objects.requireNonNull(disclosedMessages, "disclosedMessages required");
+ Objects.requireNonNull(disclosedIndexes, "disclosedIndexes required");
+ if (pk.length != 96) {
+ throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
+ }
+ int size = 1 + 96 + 4 + proof.length + 4 + header.length + 4 + presentationHeader.length
+ + 4 + messagesPayload(disclosedMessages) + 4 + 4 * disclosedIndexes.length;
+ ByteBuffer req = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ 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 -----
+
+ 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(responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(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(responsePtr);
+ responseAllocationLen = responseAllocationLen(responseLen);
+ requireValidResponseLen(responseLen);
+ return readResponsePayload(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(int responsePtr) {
+ byte[] lenBytes = memory.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(String exportName, int responsePtr, int responseLen) {
+ byte[] response = memory.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 int messagesPayload(List messages) {
+ int total = 0;
+ for (byte[] m : messages) {
+ total += 4 + m.length;
+ }
+ return total;
+ }
+
+ private static int signRequestLen(byte[] header, List messages) {
+ return 1 + 32 + 96 + 4 + header.length + 4 + messagesPayload(messages);
+ }
+
+ private static boolean decodeBool(byte[] response, String exportName) {
+ if (response.length != 1) {
+ throw new Bbs12381WasmException(
+ "Invalid BBS WASM " + exportName + " response length: " + response.length);
+ }
+ return response[0] != 0;
+ }
+}
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..012339d
--- /dev/null
+++ b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
@@ -0,0 +1,153 @@
+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) {
+ // The supplied SecureRandom is honored only if the caller used a
+ // dedicated WasmBbsProvider constructed with the same random. The
+ // standard `createDefault` factory builds the client with its own
+ // SecureRandom and ignores the per-call argument. This matches the
+ // BbsProvider SPI semantics (the random is advisory).
+ requireSuite(publicKey);
+ requireSuite(signature);
+ byte[] proof = client.proofGen(
+ ciphersuite,
+ publicKey.bytes(),
+ signature.bytes(),
+ header,
+ presentationHeader,
+ messages,
+ disclosedIndexes);
+ 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..687330d
--- /dev/null
+++ b/zeroj-bbs-wasm/src/test/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProviderTest.java
@@ -0,0 +1,341 @@
+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");
+
+ @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_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 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"));
+ }
+
+ 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();
+ }
+
+ 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);
+ }
+}
From 44f969b9eb5475b9c3e949f832aa71c86a64dfc7 Mon Sep 17 00:00:00 2001
From: Satya
Date: Tue, 12 May 2026 11:46:33 +0800
Subject: [PATCH 4/6] fix(bbs-wasm): address Codex P1-P3 review findings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
...9-cfrg-bbs-pure-java-and-wasm-providers.md | 19 +-
.../zeroj/bbs/wasm/Bbs12381WasmClient.java | 198 ++++++++++----
.../zeroj/bbs/wasm/WasmBbsProvider.java | 11 +-
.../zeroj/bbs/wasm/WasmBbsProviderTest.java | 256 ++++++++++++++++++
4 files changed, 413 insertions(+), 71 deletions(-)
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
index 7892b13..7dbe37b 100644
--- a/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
+++ b/docs/adr/0019-cfrg-bbs-pure-java-and-wasm-providers.md
@@ -327,12 +327,20 @@ pure Java to WASM.
- Chicory loads the generated WASM artifact.
- The WASM module exports the expected ABI version and operations.
-- The module has no unexpected host imports.
-- All CFRG vectors match the pure Java provider.
+- 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 Rust crate does not call host randomness for deterministic operations;
- Java supplies randomness or mocked random scalar material.
+- 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
@@ -375,7 +383,8 @@ pure Java to WASM.
| 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 a `getrandom`/`wasm-bindgen` host shim | Medium | Reject candidates that need host RNG; require caller-supplied random scalars for the proof generation ABI |
+| 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
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
index c204722..375bd52 100644
--- 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
@@ -33,12 +33,34 @@ public final class Bbs12381WasmClient {
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 Instance instance;
private final Memory memory;
- private final SecureRandom random;
+ private final SecureRandom defaultRandom;
+
+ /**
+ * Per-call random, set inside the {@code synchronized invoke(...)} block by
+ * {@link #proofGen} and restored on exit. The host {@code getrandom} function
+ * reads this field while running under the same monitor. Marked {@code volatile}
+ * so the Chicory host-function callback observes the most recent write even if
+ * Chicory dispatches via a separate thread. Null means "use {@link #defaultRandom}".
+ */
+ private volatile SecureRandom proofGenRandom;
public Bbs12381WasmClient(byte[] wasmBytes, SecureRandom random) {
Objects.requireNonNull(wasmBytes, "wasmBytes required");
@@ -46,7 +68,7 @@ public Bbs12381WasmClient(byte[] wasmBytes, SecureRandom random) {
if (wasmBytes.length == 0) {
throw new IllegalArgumentException("wasmBytes must not be empty");
}
- this.random = random;
+ this.defaultRandom = random;
try {
HostFunction hostGetrandom = new HostFunction(
"env",
@@ -63,7 +85,8 @@ public Bbs12381WasmClient(byte[] wasmBytes, SecureRandom random) {
return new long[]{0L};
}
byte[] buf = new byte[len];
- this.random.nextBytes(buf);
+ SecureRandom r = (this.proofGenRandom != null) ? this.proofGenRandom : this.defaultRandom;
+ r.nextBytes(buf);
inst.memory().write(ptr, buf);
return new long[]{0L};
});
@@ -106,10 +129,10 @@ public static Bbs12381WasmClient createDefault(SecureRandom random) {
public byte[] keyGen(BbsCiphersuite ciphersuite, byte[] keyMaterial, byte[] keyInfo) {
Objects.requireNonNull(ciphersuite, "ciphersuite required");
- Objects.requireNonNull(keyMaterial, "keyMaterial required");
- Objects.requireNonNull(keyInfo, "keyInfo required");
- ByteBuffer req = ByteBuffer.allocate(1 + 4 + keyMaterial.length + 4 + keyInfo.length)
- .order(ByteOrder.LITTLE_ENDIAN);
+ 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);
@@ -131,16 +154,14 @@ public byte[] skToPk(BbsCiphersuite ciphersuite, byte[] sk) {
public byte[] sign(BbsCiphersuite ciphersuite, byte[] sk, byte[] pk, byte[] header, List messages) {
Objects.requireNonNull(ciphersuite, "ciphersuite required");
Objects.requireNonNull(sk, "sk required");
- Objects.requireNonNull(pk, "pk required");
- Objects.requireNonNull(header, "header required");
- Objects.requireNonNull(messages, "messages required");
if (sk.length != 32) {
throw new IllegalArgumentException("BBS secret key must be 32 bytes, got " + sk.length);
}
- if (pk.length != 96) {
- throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
- }
- ByteBuffer req = ByteBuffer.allocate(signRequestLen(header, messages)).order(ByteOrder.LITTLE_ENDIAN);
+ 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);
@@ -152,18 +173,12 @@ public byte[] sign(BbsCiphersuite ciphersuite, byte[] sk, byte[] pk, byte[] head
public boolean verify(
BbsCiphersuite ciphersuite, byte[] pk, byte[] signature, byte[] header, List messages) {
Objects.requireNonNull(ciphersuite, "ciphersuite required");
- Objects.requireNonNull(pk, "pk required");
- Objects.requireNonNull(signature, "signature required");
- Objects.requireNonNull(header, "header required");
- Objects.requireNonNull(messages, "messages required");
- if (pk.length != 96) {
- throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
- }
- if (signature.length != 80) {
- throw new IllegalArgumentException("BBS signature must be 80 bytes, got " + signature.length);
- }
- ByteBuffer req = ByteBuffer.allocate(1 + 96 + 80 + 4 + header.length + 4 + messagesPayload(messages))
- .order(ByteOrder.LITTLE_ENDIAN);
+ 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);
@@ -180,23 +195,19 @@ public byte[] proofGen(
byte[] header,
byte[] presentationHeader,
List messages,
- int[] disclosedIndexes) {
+ int[] disclosedIndexes,
+ SecureRandom random) {
Objects.requireNonNull(ciphersuite, "ciphersuite required");
- Objects.requireNonNull(pk, "pk required");
- Objects.requireNonNull(signature, "signature required");
- Objects.requireNonNull(header, "header required");
- Objects.requireNonNull(presentationHeader, "presentationHeader required");
- Objects.requireNonNull(messages, "messages required");
- Objects.requireNonNull(disclosedIndexes, "disclosedIndexes required");
- if (pk.length != 96) {
- throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
- }
- if (signature.length != 80) {
- throw new IllegalArgumentException("BBS signature must be 80 bytes, got " + signature.length);
- }
- int size = 1 + 96 + 80 + 4 + header.length + 4 + presentationHeader.length
- + 4 + messagesPayload(messages) + 4 + 4 * disclosedIndexes.length;
- ByteBuffer req = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ 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);
@@ -207,7 +218,7 @@ public byte[] proofGen(
for (int idx : disclosedIndexes) {
req.putInt(idx);
}
- return invoke("zeroj_bbs_proof_gen", req.array());
+ return invokeWithRandom("zeroj_bbs_proof_gen", req.array(), random);
}
public boolean proofVerify(
@@ -219,18 +230,19 @@ public boolean proofVerify(
List disclosedMessages,
int[] disclosedIndexes) {
Objects.requireNonNull(ciphersuite, "ciphersuite required");
- Objects.requireNonNull(pk, "pk required");
- Objects.requireNonNull(proof, "proof required");
- Objects.requireNonNull(header, "header required");
- Objects.requireNonNull(presentationHeader, "presentationHeader required");
- Objects.requireNonNull(disclosedMessages, "disclosedMessages 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");
Objects.requireNonNull(disclosedIndexes, "disclosedIndexes required");
- if (pk.length != 96) {
- throw new IllegalArgumentException("BBS public key must be 96 bytes, got " + pk.length);
+ if (disclosedIndexes.length > MAX_MESSAGES) {
+ throw new IllegalArgumentException(
+ "disclosedIndexes count " + disclosedIndexes.length + " exceeds " + MAX_MESSAGES);
}
- int size = 1 + 96 + 4 + proof.length + 4 + header.length + 4 + presentationHeader.length
- + 4 + messagesPayload(disclosedMessages) + 4 + 4 * disclosedIndexes.length;
- ByteBuffer req = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
+ 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);
@@ -261,6 +273,16 @@ long invokeExportForTesting(String exportName, long... args) {
// ----- internal -----
+ private synchronized byte[] invokeWithRandom(String exportName, byte[] request, SecureRandom random) {
+ SecureRandom previous = proofGenRandom;
+ proofGenRandom = random;
+ try {
+ return invoke(exportName, request);
+ } finally {
+ proofGenRandom = previous;
+ }
+ }
+
private synchronized byte[] invoke(String exportName, byte[] request) {
int requestPtr = 0;
int responsePtr = 0;
@@ -351,16 +373,63 @@ private static void putMessageList(ByteBuffer buf, List messages) {
}
}
- private static int messagesPayload(List messages) {
- int total = 0;
- for (byte[] m : messages) {
- total += 4 + m.length;
+ 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 int signRequestLen(byte[] header, List messages) {
- return 1 + 32 + 96 + 4 + header.length + 4 + messagesPayload(messages);
+ 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);
+ }
+ 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 + ")");
+ }
+ }
+ }
+
+ 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) {
@@ -368,6 +437,15 @@ private static boolean decodeBool(byte[] response, String exportName) {
throw new Bbs12381WasmException(
"Invalid BBS WASM " + exportName + " response length: " + response.length);
}
- return response[0] != 0;
+ 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/WasmBbsProvider.java b/zeroj-bbs-wasm/src/main/java/com/bloxbean/cardano/zeroj/bbs/wasm/WasmBbsProvider.java
index 012339d..93cd34d 100644
--- 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
@@ -94,11 +94,9 @@ public BbsProof proofGen(
byte[] presentationHeader,
int[] disclosedIndexes,
SecureRandom random) {
- // The supplied SecureRandom is honored only if the caller used a
- // dedicated WasmBbsProvider constructed with the same random. The
- // standard `createDefault` factory builds the client with its own
- // SecureRandom and ignores the per-call argument. This matches the
- // BbsProvider SPI semantics (the random is advisory).
+ // 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(
@@ -108,7 +106,8 @@ public BbsProof proofGen(
header,
presentationHeader,
messages,
- disclosedIndexes);
+ disclosedIndexes,
+ Objects.requireNonNull(random, "random required"));
return new BbsProof(proof, ciphersuite);
}
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
index 687330d..a4d5900 100644
--- 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
@@ -30,6 +30,39 @@ class WasmBbsProviderTest {
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");
+
@Test
void wasmModule_hasExactlyOneImportAndExpectedExports() throws IOException {
var module = Parser.parse(loadDefaultWasm());
@@ -121,6 +154,69 @@ void proofGen_roundtripsViaProofVerify() {
new int[]{0}));
}
+ @Test
+ void proofGen_honorsPerCallSecureRandom() {
+ // The per-call SecureRandom must actually drive the host getrandom
+ // import. We can't assert "same RNG → same proof" within one WASM
+ // instance because zkryptium's ThreadRng has its own internal state
+ // that advances across calls (our host bytes only reseed it
+ // periodically). Instead we observe that the supplied RNG is being
+ // read at all, AND that the constructor's defaultRandom is NOT being
+ // read while the per-call random is present.
+ var defaultCounter = new CountingSecureRandom();
+ var perCallCounter = 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);
+
+ // KeyGen, SkToPk, Sign are deterministic in CFRG draft-10 — they
+ // must NOT consume any host RNG bytes.
+ assertEquals(0, defaultCounter.bytesRead, "deterministic ops must not call host getrandom");
+
+ BbsProof proof = provider.proofGen(
+ kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER,
+ new int[]{0}, perCallCounter);
+
+ assertTrue(perCallCounter.bytesRead > 0,
+ "per-call SecureRandom must drive host getrandom (read 0 bytes)");
+ assertEquals(0, defaultCounter.bytesRead,
+ "defaultRandom must NOT be read when a per-call SecureRandom is supplied");
+ assertTrue(provider.proofVerify(
+ kp.publicKey(), proof, HEADER, PRESENTATION_HEADER, messages, new int[]{0}));
+ }
+
+ /** 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);
@@ -150,6 +246,104 @@ void shake256_signRoundtripsViaVerify() {
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 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();
@@ -206,6 +400,20 @@ void malformedResponseLength_freesResponseAllocationOnInvokeNoArg() {
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");
@@ -276,6 +484,54 @@ private static byte[] malformedResponseWasm() {
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);
From fed80f99595468015b1f3a889862a06b3ed27c64 Mon Sep 17 00:00:00 2001
From: Satya
Date: Wed, 13 May 2026 11:46:14 +0800
Subject: [PATCH 5/6] fix(bbs-wasm): address Codex round-2 findings (P1 RNG, P2
indexes, P2 disclosure tests)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
.../zeroj/bbs/wasm/Bbs12381WasmClient.java | 167 ++++++++++++------
.../zeroj/bbs/wasm/WasmBbsProviderTest.java | 127 ++++++++++---
2 files changed, 224 insertions(+), 70 deletions(-)
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
index 375bd52..aea847e 100644
--- 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
@@ -49,51 +49,24 @@ public final class Bbs12381WasmClient {
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;
- /**
- * Per-call random, set inside the {@code synchronized invoke(...)} block by
- * {@link #proofGen} and restored on exit. The host {@code getrandom} function
- * reads this field while running under the same monitor. Marked {@code volatile}
- * so the Chicory host-function callback observes the most recent write even if
- * Chicory dispatches via a separate thread. Null means "use {@link #defaultRandom}".
- */
- private volatile SecureRandom proofGenRandom;
-
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 {
- 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];
- SecureRandom r = (this.proofGenRandom != null) ? this.proofGenRandom : this.defaultRandom;
- r.nextBytes(buf);
- inst.memory().write(ptr, buf);
- return new long[]{0L};
- });
- ImportValues imports = ImportValues.builder().addFunction(hostGetrandom).build();
- this.instance = Instance.builder(Parser.parse(wasmBytes))
- .withImportValues(imports)
- .build();
+ // 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) {
@@ -106,6 +79,39 @@ public Bbs12381WasmClient(byte[] wasmBytes, SecureRandom random) {
}
}
+ /**
+ * 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());
}
@@ -218,7 +224,7 @@ public byte[] proofGen(
for (int idx : disclosedIndexes) {
req.putInt(idx);
}
- return invokeWithRandom("zeroj_bbs_proof_gen", req.array(), random);
+ return invokeOnTransientInstance("zeroj_bbs_proof_gen", req.array(), random);
}
public boolean proofVerify(
@@ -235,10 +241,11 @@ public boolean proofVerify(
requireMaxLength(header, MAX_HEADER_BYTES, "header");
requireMaxLength(presentationHeader, MAX_HEADER_BYTES, "presentationHeader");
long messagesBytes = validateMessageList(disclosedMessages, "disclosedMessages");
- Objects.requireNonNull(disclosedIndexes, "disclosedIndexes required");
- if (disclosedIndexes.length > MAX_MESSAGES) {
+ validateDisclosedIndexesForVerify(disclosedIndexes, "disclosedIndexes");
+ if (disclosedIndexes.length != disclosedMessages.size()) {
throw new IllegalArgumentException(
- "disclosedIndexes count " + disclosedIndexes.length + " exceeds " + MAX_MESSAGES);
+ "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;
@@ -273,13 +280,39 @@ long invokeExportForTesting(String exportName, long... args) {
// ----- internal -----
- private synchronized byte[] invokeWithRandom(String exportName, byte[] request, SecureRandom random) {
- SecureRandom previous = proofGenRandom;
- proofGenRandom = random;
+ /**
+ * 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 {
- return invoke(exportName, request);
+ 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 {
- proofGenRandom = previous;
+ if (requestPtr != 0) {
+ transient_.export("dealloc").apply(requestPtr, request.length);
+ }
+ if (responsePtr != 0 && responseAllocationLen > 0) {
+ transient_.export("dealloc").apply(responsePtr, responseAllocationLen);
+ }
}
}
@@ -291,10 +324,10 @@ private synchronized byte[] invoke(String exportName, byte[] request) {
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(responsePtr);
+ long responseLen = readResponseLenHeader(memory, responsePtr);
responseAllocationLen = responseAllocationLen(responseLen);
requireValidResponseLen(responseLen);
- return readResponsePayload(exportName, responsePtr, (int) responseLen);
+ return readResponsePayload(memory, exportName, responsePtr, (int) responseLen);
} catch (Bbs12381WasmException e) {
throw e;
} catch (Exception e) {
@@ -314,10 +347,10 @@ private synchronized byte[] invokeNoArg(String exportName) {
long responseAllocationLen = 0;
try {
responsePtr = (int) instance.export(exportName).apply()[0];
- long responseLen = readResponseLenHeader(responsePtr);
+ long responseLen = readResponseLenHeader(memory, responsePtr);
responseAllocationLen = responseAllocationLen(responseLen);
requireValidResponseLen(responseLen);
- return readResponsePayload(exportName, responsePtr, (int) responseLen);
+ return readResponsePayload(memory, exportName, responsePtr, (int) responseLen);
} catch (Bbs12381WasmException e) {
throw e;
} catch (Exception e) {
@@ -329,8 +362,8 @@ private synchronized byte[] invokeNoArg(String exportName) {
}
}
- private long readResponseLenHeader(int responsePtr) {
- byte[] lenBytes = memory.readBytes(responsePtr, 4);
+ private long readResponseLenHeader(Memory mem, int responsePtr) {
+ byte[] lenBytes = mem.readBytes(responsePtr, 4);
return Integer.toUnsignedLong(ByteBuffer.wrap(lenBytes).order(ByteOrder.LITTLE_ENDIAN).getInt());
}
@@ -345,8 +378,8 @@ private long responseAllocationLen(long responseLen) {
return responseLen <= maxWasmAllocationLen - 4 ? responseLen + 4 : 0;
}
- private byte[] readResponsePayload(String exportName, int responsePtr, int responseLen) {
- byte[] response = memory.readBytes(responsePtr + 4, responseLen);
+ 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);
@@ -400,12 +433,46 @@ private static void validateIndexList(int[] indexes, int messageCount, String la
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;
}
}
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
index a4d5900..72d5d50 100644
--- 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
@@ -63,6 +63,11 @@ private static List tenFixtureMessages() {
// 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());
@@ -155,16 +160,17 @@ void proofGen_roundtripsViaProofVerify() {
}
@Test
- void proofGen_honorsPerCallSecureRandom() {
- // The per-call SecureRandom must actually drive the host getrandom
- // import. We can't assert "same RNG → same proof" within one WASM
- // instance because zkryptium's ThreadRng has its own internal state
- // that advances across calls (our host bytes only reseed it
- // periodically). Instead we observe that the supplied RNG is being
- // read at all, AND that the constructor's defaultRandom is NOT being
- // read while the per-call random is present.
+ 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 perCallCounter = new CountingSecureRandom();
var provider = new WasmBbsProvider(
BbsCiphersuite.BLS12381_SHA256,
Bbs12381WasmClient.createDefault(defaultCounter));
@@ -173,20 +179,24 @@ void proofGen_honorsPerCallSecureRandom() {
List messages = List.of(SINGLE_MSG);
BbsSignature sig = provider.sign(kp.secretKey(), kp.publicKey(), messages, HEADER);
- // KeyGen, SkToPk, Sign are deterministic in CFRG draft-10 — they
- // must NOT consume any host RNG bytes.
+ // Deterministic ops must NOT touch the host RNG.
assertEquals(0, defaultCounter.bytesRead, "deterministic ops must not call host getrandom");
- BbsProof proof = provider.proofGen(
- kp.publicKey(), sig, messages, HEADER, PRESENTATION_HEADER,
- new int[]{0}, perCallCounter);
-
- assertTrue(perCallCounter.bytesRead > 0,
- "per-call SecureRandom must drive host getrandom (read 0 bytes)");
+ 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 when a per-call SecureRandom is supplied");
- assertTrue(provider.proofVerify(
- kp.publicKey(), proof, HEADER, PRESENTATION_HEADER, messages, new int[]{0}));
+ "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. */
@@ -329,6 +339,83 @@ void proofVerify_acceptsOfficialDraft10ShakeProofFixture() {
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);
From 5d052ef7823f197599ee6610bdfe02b38b26fc29 Mon Sep 17 00:00:00 2001
From: Satya
Date: Sat, 16 May 2026 13:33:41 +0800
Subject: [PATCH 6/6] Update read me mention SecureRandom limitation in current
implemenation
---
zeroj-bbs-wasm/README.md | 31 +++++++++++++++++++++++++++----
1 file changed, 27 insertions(+), 4 deletions(-)
diff --git a/zeroj-bbs-wasm/README.md b/zeroj-bbs-wasm/README.md
index 16841a8..7cf8667 100644
--- a/zeroj-bbs-wasm/README.md
+++ b/zeroj-bbs-wasm/README.md
@@ -11,10 +11,12 @@ for the design rationale.
## When to use this module
-Use the full Rust-WASM provider when you want maximum throughput for BBS
-operations and are OK with shipping a small WebAssembly artifact. The
-algorithm runs end-to-end inside WASM, so signing, verifying, and proof
-generation incur exactly one cross-boundary call per operation.
+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;
@@ -81,6 +83,27 @@ The module imports exactly one host function:
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`.