diff --git a/docs/adr/0021-bls12381-review-and-hardening.md b/docs/adr/0021-bls12381-review-and-hardening.md
new file mode 100644
index 0000000..4dbc870
--- /dev/null
+++ b/docs/adr/0021-bls12381-review-and-hardening.md
@@ -0,0 +1,219 @@
+# ADR-0021: BLS12-381 Implementation Review Outcomes and Hardening Posture
+
+## Status
+Proposed
+
+## Date
+2026-06-10
+
+## Context
+
+`zeroj-bls12381` (ADR-0018) is the shared pure-Java BLS12-381 primitive module:
+tower fields (`Fp`/`Fp2`/`Fp6`/`Fp12` over `BigInteger` plus a Montgomery
+`MontFp381`/`MontFp2_381`/`MontFr381` layer), G1/G2 affine and Jacobian arithmetic, the
+optimal ate pairing, ZCash-format point codecs, RFC 9380 hash-to-curve, and a
+`Bls12381Provider` SPI. It underpins the Groth16/PlonK verifiers (ADR-0012) and the CFRG
+BBS implementation (ADR-0019), and must remain compatible with on-chain (Plutus CIP-0381)
+and IETF semantics.
+
+We performed a focused correctness/security/quality review of the module (manual read of
+all source + tests, plus an adversarially-verified multi-agent pass that recomputed every
+hardcoded constant with exact big-integer arithmetic and re-derived the field/curve/pairing
+math). This ADR records what the review concluded and the decisions we are taking as a
+result. It does **not** change the module's external architecture (that remains as decided
+in ADR-0018/0019); it codifies a hardening posture and a set of gated follow-ups.
+
+### What the review confirmed (no action beyond locking it in)
+
+- **Constants are bit-exact.** `p`, `r`, `INV` for both fields, `R`, `R²`, the G1/G2
+ generators (on-curve and in the prime-order subgroup), curve `b = 4`, twist `b' = 4(1+u)`,
+ RFC 9380 `h_eff` cofactors, and the SSWU `Z`/`A'`/`B'` and isogeny coefficient tables all
+ match canonical values.
+- **Arithmetic is correct.** CIOS Montgomery multiply, carry/borrow propagation, the
+ `Fp6`/`Fp12` tower, and the Jacobian `dbl-2009-l` / `add-2007-bl` formulas were verified.
+ The Montgomery-ladder invariant `R1 − R0 = P` holds.
+- **Pairing is correct.** The final exponent `(p²+1)(p⁴−p²+1)/r` composed with `f^(p⁶−1)`
+ reduces to `(p¹²−1)/r`; the negative-`x` conjugation is the standard trick. Bilinearity and
+ non-degeneracy were confirmed by direct computation.
+- **Codecs and hash-to-curve are standards-correct.** Codecs match the ZCash known-answer
+ vectors and enforce curve + prime-order-subgroup membership on the checked decoders;
+ hash-to-curve matches the official RFC 9380 `QUUX-V01-CS02` G1/G2 RO+NU vectors.
+
+There are **no correctness bugs on the normal factory/arithmetic code paths.**
+
+### What the review surfaced as needing a decision
+
+1. **Overstated constant-time guarantee on a path BBS+ uses for secrets.**
+ `JacobianG1/G2BLS381.ctScalarMul` (reached via `PureJavaBls12381Provider.g1/g2SecretScalarMul`)
+ is documented as preventing timing side-channels on secret scalars, but it is not
+ constant-time: leading-zero scalar bits leave `r0 = INFINITY` and take `add`/`doublePoint`
+ early-return fast paths (leaks MSB position), the Montgomery conditional subtraction
+ branches on data, and bit selection is a secret-dependent branch over `BigInteger.testBit`
+ (not a branchless select). `CfrgBbsCore` routes the BBS **secret signing key**, signature
+ scalars, and undisclosed-message scalars through this path. ADR-0019 §5 already flagged the
+ JVM path as "fixed-schedule … not a full constant-time guarantee"; the review confirms the
+ gap is real and that the in-code documentation currently overstates it.
+
+2. **Latent foot-guns behind public low-level APIs (not reachable on normal paths).**
+ - `MontFp381.fromMontLimbs` is public and unchecked; feeding non-canonical limbs (≥ p)
+ into `montMul` can violate the reduced-residue invariant and produce a silently wrong
+ field element. The package-private `MontFr381.fromMontLimbs` has the same invariant
+ shape and should be guarded defensively inside the field package.
+ - `JacobianG1/G2BLS381.ctScalarMul` runs a fixed 256-bit ladder with no length guard, so a
+ scalar ≥ 2²⁵⁶ is silently truncated to its low 256 bits. Masked today because the
+ provider reduces mod `r` first, but the EC-level method is public.
+
+3. **Pairing test coverage cannot detect a regression.** `BLS12381PairingTest` asserts only
+ self-consistent identities (`e(P,Q)·e(−P,Q)=1`, conjugate pairs, `finalExp(1)=1`). These
+ pass even for a degenerate constant-`1` pairing. There is no bilinearity, non-degeneracy,
+ or known-answer assertion. The pairing is exercised end-to-end by the Groth16/PlonK
+ verifier modules with real proof vectors, but this module does not independently lock it
+ in. Related codec/test gaps: the compressed sign/sort bit (`0x20`) is never checked with a
+ discriminating point, the G2 `ctScalarMul`↔`scalarMul` cross-check is absent (G1 has one),
+ `Fp6` has no direct test and `Fp12` has only light property coverage, and the
+ SHAKE-256/XOF hash-to-curve family + hand-rolled Keccak-f1600 have no direct
+ `zeroj-bls12381` known-answer vector. BBS has indirect SHAKE-256 fixture coverage, but the
+ primitive module still needs its own gates.
+
+4. **Performance is correctness-first, not optimized.** The final exponentiation hard part
+ uses a generic ~4500-bit `BigInteger` square-and-multiply over the non-Montgomery field
+ (~100 ms/pairing, ~100× slower than an addition-chain + cyclotomic-squaring
+ implementation); subgroup membership is a naive `[r]P` scalar-mul with a `modInverse` per
+ step, run by `requireValid` on every checked deserialization and SPI add/negate/scalar-mul.
+ The performance-critical pairing/codec paths run on the `BigInteger` field stack while the
+ faster Montgomery stack is used only by `ctScalarMul` (dual-stack divergence).
+
+## Decision
+
+### 1. Treat the pure-Java provider as correctness-first, not side-channel-hardened
+
+The pure-Java `zeroj-bls12381` provider is the portable, correctness-validated default for
+**public-input** operations (verification, encoding, hashing, public scalar multiplication).
+It is explicitly **not** a constant-time implementation. We will:
+
+- Correct the Javadoc on `ctScalarMul` and `g1/g2SecretScalarMul` to state the real, limited
+ guarantee (uniform operation schedule only; underlying field/point ops and bit access are
+ variable-time on the JVM), removing language that implies timing-side-channel resistance.
+- Keep `g1/g2SecretScalarMul` as the secret-scalar boundary (so callers route secrets through
+ a named seam), but document that production secret-bearing workloads (BBS+ key generation
+ and signing) should select the native `zeroj-blst` provider via
+ `BbsProviders.withBlsProvider(...)`, which genuinely overrides these operations.
+- Defer a genuinely constant-time pure-Java path (branchless conditional select, fixed-length
+ limb scalar, branchless field reduction) to a future ADR; it is not required for the
+ verifier-first product and is hard to guarantee under JIT/GC. This reaffirms and tightens
+ ADR-0019 §5.
+
+### 2. Guard the public low-level foot-guns
+
+- `MontFp381.fromMontLimbs` (and the package-private `MontFr381` equivalent) must
+ canonicalize or reject non-reduced inputs so the `< modulus` invariant every other path
+ relies on always holds.
+- `JacobianG1/G2BLS381.ctScalarMul` must reduce the scalar mod `r` at entry, or reject
+ scalars with `bitLength() > 256`, so the ladder cannot silently truncate.
+
+These are defensive corrections to internal/SPI-adjacent APIs; no public verifier behavior
+changes.
+
+### 3. Make pairing correctness a locked-in test gate
+
+Add to `zeroj-bls12381` the tests that pin pairing correctness independently of the verifier
+modules: non-degeneracy (`e(G1,G2) ≠ 1`), `e(G1,G2)^r = 1`, bilinearity over non-trivial
+scalars (`e([a]G1,G2) = e(G1,G2)^a`, `e([a]G1,[b]G2) = e(G1,G2)^{ab}`), and at least one
+hardcoded `e(G1,G2)` Fp12 regression vector in ZeroJ's tower representation. The current
+Fp12 vector is self-pinned from the reviewed implementation; an externally sourced
+blst/zkcrypto coefficient vector should replace or corroborate it if a compatible
+serialization/layout is added. Add a discriminating compressed sign-bit (`0x20`) round-trip
+test, a G2 `ctScalarMul`↔`scalarMul` cross-check, direct `Fp6`/`Fp12` property tests, and a
+SHAKE-256/Keccak known-answer vector. These become release gates for the module.
+
+### 4. Sequence performance hardening after correctness is locked
+
+Performance work — addition-chain + cyclotomic-squaring final exponentiation, endomorphism-
+based (GLV/ψ) subgroup checks, hoisting the recomputed hard-exponent constant, dropping the
+redundant `requireValid` after cofactor clearing, and converging the dual field stacks — is a
+post-correctness roadmap item, not a release blocker. It must be done only after the Decision
+3 gates exist, and re-validated against the Groth16/PlonK real-proof vectors. The currently
+unused `FROBENIUS_COEFF_X/Y` constants in `BLS12381Pairing` are either wired into the
+optimized final exponentiation or removed as dead code at that time.
+
+### 5. Accept the remaining items as documented, not blocking
+
+The naive-but-correct subgroup check, the dual field stack, the duplicated `r` literal, and
+the over-broad `reflect-config.json` are accepted as-is for the current release; they are
+quality/performance items tracked for the hardening pass, not correctness or security
+defects.
+
+## Consequences
+
+### Easier
+- Downstream protocols get an explicit, honest security contract: pure Java for public-input
+ verification; native `blst` for secret-bearing operations.
+- A future refactor (notably the addition-chain final exponentiation) can proceed safely
+ because pairing correctness is pinned by bilinearity/non-degeneracy/KAT tests.
+- Public low-level misuse (`fromMontLimbs`, over-large `ctScalarMul`) fails loudly instead of
+ producing silently wrong results.
+
+### Harder
+- BBS+ production guidance now has an explicit provider-selection requirement for secret-key
+ workloads, which callers must follow.
+- The module gains additional test-maintenance surface (pairing KAT, SHAKE KAT, tower tests).
+
+### Neutral
+- No external API or on-chain verifier behavior changes; the verifier-first architecture
+ (ADR-0001) and crypto/policy separation (ADR-0006) are unaffected.
+- The performance roadmap is acknowledged but intentionally deferred.
+
+## Test Plan
+
+- **Pairing gates:** non-degeneracy, `e^r = 1`, bilinearity for several scalars, and a
+ hardcoded `e(G1,G2)` Fp12 regression vector. Sanity-check that these fail against an
+ intentionally degenerate pairing before committing. Prefer an external blst/zkcrypto
+ coefficient vector when a compatible Fp12 serialization/layout is available.
+- **Codec gate:** compress `P` and `−P`, assert the encodings differ only in `0x20` and each
+ decodes to the correct point; flip `0x20` manually and assert negation.
+- **Foot-gun gates:** `fromMontLimbs` on limbs ≥ p is rejected/canonicalized; `ctScalarMul`
+ on a scalar ≥ 2²⁵⁶ is rejected or reduced (not silently wrong).
+- **Coverage:** G2 `ctScalarMul`↔`scalarMul` equality; `Fp6`/`Fp12` mul/square/inverse
+ property + cross-validation tests; a SHAKE-256/Keccak and a real `hashToScalar` KAT.
+- **Regression:** Groth16/PlonK verifier module tests (real proof vectors in
+ `zeroj-test-vectors`) and the BBS provider conformance suite must stay green after any
+ Decision 2/4 change.
+
+## Implementation Plan
+
+1. Add regression tests for the confirmed security gates before changing behavior:
+ pairing non-degeneracy, `e(G1,G2)^r = 1`, bilinearity, a hardcoded `e(G1,G2)` Fp12
+ regression vector, the compressed sign/sort-bit flip, G2 `ctScalarMul`↔`scalarMul`,
+ `ctScalarMul(2^256 + 1)`, and a fixed non-canonical `MontFp381.fromMontLimbs`
+ multiplication case.
+2. Add the `fromMontLimbs` canonicalization/rejection and `ctScalarMul` length guard
+ (Decision 2). Prefer rejecting non-reduced Montgomery limbs and rejecting EC-level
+ `ctScalarMul` scalars with `bitLength() > 256`; the SPI provider already reduces modulo
+ `r` before calling the EC-level method.
+3. Correct the constant-time Javadoc/contract and document the blst-for-secrets guidance
+ (Decision 1); cross-reference from `zeroj-bbs`.
+4. Add the remaining primitive coverage (`Fp6`/stronger `Fp12`, SHAKE/XOF hash-to-curve,
+ direct SHAKE/Keccak expander KATs) (Decision 3).
+5. Schedule the performance hardening (Decision 4) as a separate, gated effort after the
+ correctness/security gates are green.
+
+## Risks
+
+| Risk | Severity | Mitigation |
+|---|---:|---|
+| Callers assume the pure-Java secret-scalar path is constant-time | High | Correct the Javadoc/contract (Decision 1); route BBS+ secret workloads to `zeroj-blst`; require an environment-specific side-channel review for high-value deployments |
+| A future pairing refactor silently breaks correctness | High | Land the bilinearity/non-degeneracy/KAT gates (Decision 3) before any optimization (Decision 4) |
+| Public `fromMontLimbs`/`ctScalarMul` misuse yields silently wrong results | Medium | Canonicalize/guard inputs (Decision 2) |
+| Performance (~100 ms/pairing) limits multi-pairing verification throughput | Medium | Addition-chain + cyclotomic-squaring final exp and endomorphism subgroup checks on the deferred roadmap (Decision 4) |
+| Test-maintenance burden grows | Low | KATs are static vectors; tower/codec tests are small and stable |
+
+## References
+
+- ADR-0001: Verifier-First Architecture
+- ADR-0006: Separation of Crypto and Policy Verification
+- ADR-0012: Pure Java Provers for Groth16 and PlonK
+- ADR-0018: Shared BLS12-381 Primitives and Optional WASM Provider
+- ADR-0019: CFRG BBS Pure Java and Optional WASM Providers (see §5, constant-time boundary)
+- RFC 9380 hash-to-curve: This delegates to the pure-Java Jacobian ladder. It has a uniform operation
+ * schedule, but it is not a JVM constant-time guarantee. This delegates to the pure-Java Jacobian ladder. It has a uniform operation
+ * schedule, but it is not a JVM constant-time guarantee. Always performs both a doubling and an addition per bit, regardless of bit value.
- * The bit only determines which accumulator receives which result. This prevents
- * timing side-channels that leak scalar bits.
Use this when the scalar is secret (e.g., blinding factors r, s in Groth16).
+ *Use this only as the pure-Java fixed-schedule path. High-value secret-bearing + * workloads should use a native provider with a stronger side-channel contract.
* - * @param scalar non-negative scalar (fixed 255-bit processing for BLS12-381 Fr) + * @param scalar non-negative scalar with {@code bitLength() <= 256} */ public JacobianG1BLS381 ctScalarMul(BigInteger scalar) { if (scalar.signum() == 0) return INFINITY; if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate()); + if (scalar.bitLength() > 256) { + throw new IllegalArgumentException("ctScalarMul scalar must fit in 256 bits"); + } if (this.isInfinity()) return INFINITY; // Montgomery ladder with a fixed operation schedule per bit. diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java index 0da4eb6..b99de9e 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381.java @@ -124,10 +124,19 @@ public JacobianG2BLS381 scalarMul(BigInteger scalar) { return result; } - /** Constant-time scalar multiplication using Montgomery ladder. */ + /** + * Fixed-schedule scalar multiplication using a Montgomery ladder. + * + *This keeps a uniform operation schedule, but it is not a JVM constant-time + * guarantee: bit access, branching, point special cases, and field reductions remain + * variable-time.
+ */ public JacobianG2BLS381 ctScalarMul(BigInteger scalar) { if (scalar.signum() == 0) return INFINITY; if (scalar.signum() < 0) return negate().ctScalarMul(scalar.negate()); + if (scalar.bitLength() > 256) { + throw new IllegalArgumentException("ctScalarMul scalar must fit in 256 bits"); + } if (this.isInfinity()) return INFINITY; JacobianG2BLS381 r0 = INFINITY; diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java index 325f2d2..7b20f6b 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381.java @@ -95,8 +95,12 @@ public static MontFp381 fromLong(long val) { /** * Creates a MontFp381 from 6 Montgomery-form limbs (little-endian: l0 is least significant). * The limbs must already be in Montgomery form — no conversion is performed. + * The represented Montgomery residue must be canonical ({@code < p}). */ public static MontFp381 fromMontLimbs(long l0, long l1, long l2, long l3, long l4, long l5) { + if (geqMod(l0, l1, l2, l3, l4, l5)) { + throw new IllegalArgumentException("Montgomery Fp limbs must be canonical"); + } return new MontFp381(l0, l1, l2, l3, l4, l5); } @@ -173,9 +177,9 @@ public MontFp381 dbl() { /** * Field inversion via Fermat's little theorem: a^{-1} = a^{p-2} mod p. * - *Uses a fixed-length square-and-multiply chain over the exponent p-2, - * providing constant-time behavior (the same number of multiplications - * regardless of the input value).
+ *Uses a fixed public exponent ({@code p - 2}). This keeps the exponent schedule + * independent of the input value, but the pure-Java field operations are not a full + * JVM constant-time guarantee.
*/ public MontFp381 inverse() { if (isZero()) throw new ArithmeticException("Cannot invert zero"); diff --git a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java index cc2332e..06af0eb 100644 --- a/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java +++ b/zeroj-bls12381/src/main/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381.java @@ -76,6 +76,9 @@ public static MontFr381 fromLong(long val) { } static MontFr381 fromMontLimbs(long l0, long l1, long l2, long l3) { + if (geqMod(l0, l1, l2, l3)) { + throw new IllegalArgumentException("Montgomery Fr limbs must be canonical"); + } return new MontFr381(l0, l1, l2, l3); } @@ -136,7 +139,8 @@ public MontFr381 square() { /** * Field inversion via Fermat's little theorem: a^{-1} = a^{r-2} mod r. - * Uses fixed-length square-and-multiply for constant-time behavior. + * Uses a fixed public exponent schedule, but the pure-Java field operations are + * not a full JVM constant-time guarantee. */ public MontFr381 inverse() { if (isZero()) throw new ArithmeticException("Cannot invert zero"); 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 index 5d63a89..bd53c06 100644 --- 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 @@ -13,7 +13,8 @@ * *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.
+ * by secret scalars must use the secret-scalar methods and select a provider with + * a side-channel contract appropriate for the deployment. */ public interface Bls12381Provider { String id(); @@ -51,14 +52,16 @@ default G2Point g2Negate(G2Point point) { G2Point g2ScalarMul(G2Point point, BigInteger scalar); /** - * Multiply a G1 point by a secret scalar using this provider's side-channel hardened path. + * Multiply a G1 point by a secret scalar using this provider's secret-scalar path. + * Consult the provider documentation for its side-channel guarantees. */ 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. + * Multiply a G2 point by a secret scalar using this provider's secret-scalar path. + * Consult the provider documentation for its side-channel guarantees. */ default G2Point g2SecretScalarMul(G2Point point, BigInteger scalar) { throw new UnsupportedOperationException(id() + " does not declare a secret-scalar G2 multiplication contract"); 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 index d274d42..3c6f24e 100644 --- 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 @@ -11,6 +11,11 @@ /** * Default provider backed by ZeroJ's pure Java BLS12-381 implementation. + * + *This provider is correctness-first and portable. Its secret-scalar methods use the + * module's fixed-schedule Java ladders, but they do not provide a full JVM constant-time + * guarantee. Production secret-bearing workloads should prefer a native provider with an + * audited side-channel contract, such as {@code zeroj-blst}.
*/ public final class PureJavaBls12381Provider implements Bls12381Provider { public static final PureJavaBls12381Provider INSTANCE = new PureJavaBls12381Provider(); 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 index 1f1d9eb..3a294ad 100644 --- 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 @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; class Bls12381CodecsTest { + private static final int SORT_FLAG = 0x20; @Test void g1Uncompressed_roundTripsGenerator() { @@ -54,6 +55,34 @@ void g2Compressed_generatorMatchesZcashEncoding() { Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2)); } + @Test + void g1Compressed_sortBitDistinguishesNegation() { + byte[] encoded = Bls12381Codecs.g1ToCompressed(Bls12381Generators.G1); + byte[] negated = Bls12381Codecs.g1ToCompressed(Bls12381Generators.G1.negate()); + + assertDiffersOnlyBySortBit(encoded, negated); + assertEquals(Bls12381Generators.G1, Bls12381Codecs.g1FromCompressed(encoded)); + assertEquals(Bls12381Generators.G1.negate(), Bls12381Codecs.g1FromCompressed(negated)); + + byte[] flipped = encoded.clone(); + flipped[0] ^= SORT_FLAG; + assertEquals(Bls12381Generators.G1.negate(), Bls12381Codecs.g1FromCompressed(flipped)); + } + + @Test + void g2Compressed_sortBitDistinguishesNegation() { + byte[] encoded = Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2); + byte[] negated = Bls12381Codecs.g2ToCompressed(Bls12381Generators.G2.negate()); + + assertDiffersOnlyBySortBit(encoded, negated); + assertEquals(Bls12381Generators.G2, Bls12381Codecs.g2FromCompressed(encoded)); + assertEquals(Bls12381Generators.G2.negate(), Bls12381Codecs.g2FromCompressed(negated)); + + byte[] flipped = encoded.clone(); + flipped[0] ^= SORT_FLAG; + assertEquals(Bls12381Generators.G2.negate(), Bls12381Codecs.g2FromCompressed(flipped)); + } + @Test void infinity_roundTrips() { assertEquals(G1Point.INFINITY, Bls12381Codecs.g1FromUncompressed(Bls12381Codecs.g1ToUncompressed(G1Point.INFINITY))); @@ -132,4 +161,13 @@ private static byte[] hexToBytes(String hex) { } return out; } + + private static void assertDiffersOnlyBySortBit(byte[] left, byte[] right) { + assertEquals(SORT_FLAG, (left[0] ^ right[0]) & 0xff); + byte[] leftMasked = left.clone(); + byte[] rightMasked = right.clone(); + leftMasked[0] &= ~SORT_FLAG; + rightMasked[0] &= ~SORT_FLAG; + assertArrayEquals(leftMasked, rightMasked); + } } 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 index 057b366..5106ead 100644 --- 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 @@ -66,6 +66,26 @@ void expandMessageXmd_usesOversizeDstReduction() { Bls12381Hash.expandMessageXmdSha256(msg, oversizedDst, 32)); } + @Test + void expandMessageXofShake256_matchesRfc9380Vector() { + byte[] msg = "abc".getBytes(StandardCharsets.US_ASCII); + byte[] dst = "QUUX-V01-CS02-with-expander-SHAKE256".getBytes(StandardCharsets.US_ASCII); + + assertArrayEquals( + hexToBytes("b39e493867e2767216792abce1f2676c197c0692aed061560ead251821808e07"), + Bls12381Hash.expandMessageXofShake256(msg, dst, 32)); + } + + @Test + void hashToScalarXofShake256_matchesOfficialBbsFixture() { + byte[] message = hexToBytes("9872ad089e452c7b6e283dfac2a80d58e8d0ff71cc4d5e310a1debdda4a45f02"); + byte[] dst = hexToBytes("4242535f424c53313233383147315f584f463a5348414b452d3235365f535357555f524f5f4832475f484d32535f4832535f"); + + assertEquals( + new BigInteger("0500031f786fde5326aa9370dd7ffe9535ec7a52cf2b8f432cad5d9acfb73cd3", 16), + Bls12381Hash.hashToScalarXofShake256(message, dst)); + } + @Test void hashToG2_matchesRfc9380EmptyMessageVector() { assertG2( @@ -125,4 +145,12 @@ private static byte[] concat(byte[] left, byte[] right) { System.arraycopy(right, 0, out, left.length, right.length); return out; } + + 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/ec/JacobianG1BLS381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java index 71fbc77..c41231e 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG1BLS381Test.java @@ -84,6 +84,14 @@ void ctScalarMul_byOrder_returnsInfinity() { assertTrue(JacobianG1BLS381.GENERATOR.ctScalarMul(R).isInfinity()); } + @Test + void ctScalarMul_rejectsScalarsAbove256Bits() { + BigInteger scalar = BigInteger.ONE.shiftLeft(256).add(BigInteger.ONE); + + assertThrows(IllegalArgumentException.class, + () -> JacobianG1BLS381.GENERATOR.ctScalarMul(scalar)); + } + @Test void crossValidate_withVerifierG1Point() { var g = JacobianG1BLS381.GENERATOR.toAffine(); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java index 707b66f..7c5175c 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/ec/JacobianG2BLS381Test.java @@ -58,4 +58,21 @@ void scalarMul_small_isOnCurve() { assertTrue(p.toAffine().isOnCurve(), k + " * G2 must be on twist curve"); } } + + @Test + void ctScalarMul_matchesScalarMul() { + for (int k = 1; k <= 10; k++) { + var expected = JacobianG2BLS381.GENERATOR.scalarMul(BigInteger.valueOf(k)).toAffine(); + var actual = JacobianG2BLS381.GENERATOR.ctScalarMul(BigInteger.valueOf(k)).toAffine(); + assertEquals(expected, actual, "ctScalarMul(" + k + ") mismatch"); + } + } + + @Test + void ctScalarMul_rejectsScalarsAbove256Bits() { + BigInteger scalar = BigInteger.ONE.shiftLeft(256).add(BigInteger.ONE); + + assertThrows(IllegalArgumentException.class, + () -> JacobianG2BLS381.GENERATOR.ctScalarMul(scalar)); + } } diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6Fp12Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6Fp12Test.java new file mode 100644 index 0000000..1209ae1 --- /dev/null +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/Fp6Fp12Test.java @@ -0,0 +1,68 @@ +package com.bloxbean.cardano.zeroj.bls12381.field; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Fp6Fp12Test { + + @Test + void fp6_squareMatchesMul() { + Fp6 a = fp6(42, 7, 3, 5, 11, 13); + + assertEquals(a.mul(a), a.square()); + } + + @Test + void fp6_inverseRoundTrip() { + Fp6 a = fp6(42, 7, 3, 5, 11, 13); + + assertEquals(Fp6.ONE, a.mul(a.inv())); + } + + @Test + void fp12_squareMatchesMul() { + Fp12 a = fp12( + fp6(42, 7, 3, 5, 11, 13), + fp6(17, 19, 23, 29, 31, 37)); + + assertEquals(a.mul(a), a.square()); + } + + @Test + void fp12_inverseRoundTrip() { + Fp12 a = fp12( + fp6(42, 7, 3, 5, 11, 13), + fp6(17, 19, 23, 29, 31, 37)); + + assertTrue(a.mul(a.inv()).isOne()); + } + + @Test + void fp12_distributesOverAddition() { + Fp12 a = fp12( + fp6(42, 7, 3, 5, 11, 13), + fp6(17, 19, 23, 29, 31, 37)); + Fp12 b = fp12( + fp6(41, 43, 47, 53, 59, 61), + fp6(67, 71, 73, 79, 83, 89)); + Fp12 c = fp12( + fp6(97, 101, 103, 107, 109, 113), + fp6(127, 131, 137, 139, 149, 151)); + + assertEquals(a.mul(b).add(a.mul(c)), a.mul(b.add(c))); + } + + private static Fp12 fp12(Fp6 c0, Fp6 c1) { + return new Fp12(c0, c1); + } + + private static Fp6 fp6(long c00, long c01, long c10, long c11, long c20, long c21) { + return new Fp6(fp2(c00, c01), fp2(c10, c11), fp2(c20, c21)); + } + + private static Fp2 fp2(long c0, long c1) { + return Fp2.of(Fp.of(c0), Fp.of(c1)); + } +} diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java index 4e6f576..37c7b97 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFp381Test.java @@ -104,6 +104,28 @@ void boundary_modulusWraps() { assertTrue(MontFp381.fromBigInteger(P.add(BigInteger.ONE)).isOne()); } + @Test + void fromMontLimbs_acceptsCanonicalLimbs() { + long[] limbs = MontFp381.ONE.toLimbs(); + + assertTrue(MontFp381.fromMontLimbs( + limbs[0], limbs[1], limbs[2], limbs[3], limbs[4], limbs[5]).isOne()); + } + + @Test + void fromMontLimbs_rejectsNonCanonicalLimbs() { + assertThrows(IllegalArgumentException.class, + () -> MontFp381.fromMontLimbs( + MontFp381.MOD0, MontFp381.MOD1, MontFp381.MOD2, + MontFp381.MOD3, MontFp381.MOD4, MontFp381.MOD5)); + + assertThrows(IllegalArgumentException.class, + () -> MontFp381.fromMontLimbs( + -8988751357500304535L, 2419637729810828715L, + 8198777404514432018L, -5182139003232109836L, + 5314520057811676541L, -622504768958473957L)); + } + @RepeatedTest(100) void square_random_matchesMul() { BigInteger a = randomFp(); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java index 67c155e..009c536 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/field/MontFr381Test.java @@ -152,6 +152,19 @@ void boundary_modulusWraps() { assertTrue(MontFr381.fromBigInteger(R.add(BigInteger.ONE)).isOne()); } + @Test + void fromMontLimbs_acceptsCanonicalLimbs() { + assertTrue(MontFr381.fromMontLimbs( + MontFr381.RONE0, MontFr381.RONE1, MontFr381.RONE2, MontFr381.RONE3).isOne()); + } + + @Test + void fromMontLimbs_rejectsNonCanonicalLimbs() { + assertThrows(IllegalArgumentException.class, + () -> MontFr381.fromMontLimbs( + MontFr381.MOD0, MontFr381.MOD1, MontFr381.MOD2, MontFr381.MOD3)); + } + private BigInteger randomFr() { byte[] bytes = new byte[32]; RNG.nextBytes(bytes); diff --git a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java index ff02cd9..f6459fd 100644 --- a/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java +++ b/zeroj-bls12381/src/test/java/com/bloxbean/cardano/zeroj/bls12381/pairing/BLS12381PairingTest.java @@ -1,5 +1,6 @@ package com.bloxbean.cardano.zeroj.bls12381.pairing; +import com.bloxbean.cardano.zeroj.bls12381.Bls12381Generators; import com.bloxbean.cardano.zeroj.bls12381.ec.*; import com.bloxbean.cardano.zeroj.bls12381.field.*; import org.junit.jupiter.api.Test; @@ -10,14 +11,8 @@ class BLS12381PairingTest { - private static final G1Point G1 = new G1Point( - Fp.of(new BigInteger("17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb", 16)), - Fp.of(new BigInteger("08b3f481e3aaa0f1a09e30ed741d8ae4fcf5e095d5d00af600db18cb2c04b3edd03cc744a2888ae40caa232946c5e7e1", 16))); - private 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)))); + private static final G1Point G1 = Bls12381Generators.G1; + private static final G2Point G2 = Bls12381Generators.G2; @Test void cyclotomicPolynomialDivisibleByR() { @@ -71,4 +66,74 @@ void pairingCheck_ePlusNegE_isOne() { new G2Point[]{G2, G2}); assertTrue(result, "e(P,Q)*e(-P,Q) must be 1"); } + + @Test + void generatorPairing_isNonDegenerateAndHasOrderR() { + var e = generatorPairing(); + + assertFalse(e.isOne(), "e(G1,G2) must not be 1"); + assertTrue(e.pow(G1Point.R).isOne(), "e(G1,G2)^r must be 1"); + } + + @Test + void generatorPairing_matchesKnownAnswer() { + assertEquals(generatorPairingKat(), generatorPairing()); + } + + @Test + void pairing_isBilinearForNonTrivialScalars() { + var e = generatorPairing(); + var a = BigInteger.valueOf(5); + var b = BigInteger.valueOf(7); + + var eAg2 = pair(G1.scalarMul(a), G2); + var eG1b = pair(G1, G2.scalarMul(b)); + var eAg1b = pair(G1.scalarMul(a), G2.scalarMul(b)); + + assertEquals(e.pow(a), eAg2, "e([a]G1,G2) must equal e(G1,G2)^a"); + assertEquals(e.pow(b), eG1b, "e(G1,[b]G2) must equal e(G1,G2)^b"); + assertEquals(e.pow(a.multiply(b)), eAg1b, "e([a]G1,[b]G2) must equal e(G1,G2)^(ab)"); + } + + private static Fp12 generatorPairing() { + return pair(G1, G2); + } + + private static Fp12 pair(G1Point p, G2Point q) { + return BLS12381Pairing.finalExponentiation(BLS12381Pairing.millerLoop(p, q)); + } + + // Self-pinned regression vector in ZeroJ's Fp12 tower layout. Bilinearity, + // non-degeneracy, and e^r checks above provide the independent correctness gates. + // Replace or corroborate this with an external coefficient vector when a compatible + // blst/zkcrypto Fp12 serialization is available. + private static Fp12 generatorPairingKat() { + return fp12( + "11619b45f61edfe3b47a15fac19442526ff489dcda25e59121d9931438907dfd448299a87dde3a649bdba96e84d54558", + "153ce14a76a53e205ba8f275ef1137c56a566f638b52d34ba3bf3bf22f277d70f76316218c0dfd583a394b8448d2be7f", + "95668fb4a02fe930ed44767834c915b283b1c6ca98c047bd4c272e9ac3f3ba6ff0b05a93e59c71fba77bce995f04692", + "16deedaa683124fe7260085184d88f7d036b86f53bb5b7f1fc5e248814782065413e7d958d17960109ea006b2afdeb5f", + "9c92cf02f3cd3d2f9d34bc44eee0dd50314ed44ca5d30ce6a9ec0539be7a86b121edc61839ccc908c4bdde256cd6048", + "111061f398efc2a97ff825b04d21089e24fd8b93a47e41e60eae7e9b2a38d54fa4dedced0811c34ce528781ab9e929c7", + "1ecfcf31c86257ab00b4709c33f1c9c4e007659dd5ffc4a735192167ce197058cfb4c94225e7f1b6c26ad9ba68f63bc", + "8890726743a1f94a8193a166800b7787744a8ad8e2f9365db76863e894b7a11d83f90d873567e9d645ccf725b32d26f", + "e61c752414ca5dfd258e9606bac08daec29b3e2c57062669556954fb227d3f1260eedf25446a086b0844bcd43646c10", + "fe63f185f56dd29150fc498bbeea78969e7e783043620db33f75a05a0a2ce5c442beaff9da195ff15164c00ab66bdde", + "10900338a92ed0b47af211636f7cfdec717b7ee43900eee9b5fc24f0000c5874d4801372db478987691c566a8c474978", + "1454814f3085f0e6602247671bc408bbce2007201536818c901dbd4d2095dd86c1ec8b888e59611f60a301af7776be3d"); + } + + private static Fp12 fp12(String... c) { + return new Fp12( + new Fp6(fp2(c[0], c[1]), fp2(c[2], c[3]), fp2(c[4], c[5])), + new Fp6(fp2(c[6], c[7]), fp2(c[8], c[9]), fp2(c[10], c[11]))); + } + + private static Fp2 fp2(String c0, String c1) { + return Fp2.of(fp(c0), fp(c1)); + } + + private static Fp fp(String hex) { + return Fp.of(new BigInteger(hex, 16)); + } } diff --git a/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCacheTest.java b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCacheTest.java new file mode 100644 index 0000000..6d864c4 --- /dev/null +++ b/zeroj-crypto/src/test/java/com/bloxbean/cardano/zeroj/crypto/setup/SetupCacheTest.java @@ -0,0 +1,29 @@ +package com.bloxbean.cardano.zeroj.crypto.setup; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SetupCacheTest { + + @TempDir + Path tempDir; + + @Test + void srsCacheRoundTripsCanonicalBls381MontgomeryLimbs() throws Exception { + var srs = PowersOfTauBLS381.generate(4); + Path path = tempDir.resolve("srs.bin"); + + SetupCache.saveSrs(srs, path); + var loaded = SetupCache.loadSrs(path); + + assertEquals(srs.power(), loaded.power()); + assertEquals(srs.tauScalar(), loaded.tauScalar()); + assertArrayEquals(srs.tauG1(), loaded.tauG1()); + assertArrayEquals(srs.tauG2(), loaded.tauG2()); + } +}