Skip to content

Commit 6fc14db

Browse files
committed
Further review of BLS implementation. Additional tests and public key validation, WipingBuffer
1 parent 3d8fa20 commit 6fc14db

10 files changed

Lines changed: 1241 additions & 29 deletions

File tree

core/src/main/java/org/bouncycastle/crypto/generators/BLSKeyPairGenerator.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,20 @@ public AsymmetricCipherKeyPair generateKeyPair()
4949
}
5050

5151
// Draw 32 bytes of IKM from the SecureRandom (the spec's minimum).
52-
// Feeding this through KeyGen / HKDF gives a uniform secret in [1, r - 1].
52+
// Feeding this through KeyGen / HKDF gives a uniform secret in
53+
// [1, r - 1].
54+
//
55+
// The try/finally wipe of ikm is load-bearing: KeyGen is a pure
56+
// deterministic function of (IKM, salt, key_info), and salt /
57+
// key_info are fixed public constants for this generator. So
58+
// anyone who reads ikm from a heap dump can derive the same sk by
59+
// re-running keyGen offline — i.e. ikm is effectively a second
60+
// copy of the secret-key bytes. Without the wipe, the bytes
61+
// survive until the next GC collects the local stack frame and
62+
// the array body; with the wipe the residence window is the body
63+
// of the try-block. (The constant-time scalar-mult work at
64+
// sign-time would be pointless if a heap inspector could just
65+
// read the seed material from a separate file.)
5366
byte[] ikm = new byte[32];
5467
try
5568
{

core/src/main/java/org/bouncycastle/crypto/params/BLSPublicKeyParameters.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
package org.bouncycastle.crypto.params;
22

3+
import org.bouncycastle.crypto.bls.BLS12_381BasicScheme;
34
import org.bouncycastle.crypto.bls.BLS12_381Serialization;
45
import org.bouncycastle.math.ec.ECPoint;
56

67
/**
78
* Public-key parameters for a BLS signature scheme. The underlying point
89
* lives on the suite-specific G1 curve (BLS12-381 G1 today; further BLS
910
* families add curves in the future).
11+
* <p>
12+
* <b>Constructor invariant.</b> Construction runs the full
13+
* draft-irtf-cfrg-bls-signature sec. 2.5 {@code KeyValidate} (on-curve +
14+
* prime-order subgroup + non-identity) on the supplied point and rejects
15+
* anything that fails. Callers and downstream consumers can therefore
16+
* rely on the type itself as the validation boundary — once an instance
17+
* exists, the point is a valid BLS public key, and verify-time code
18+
* (e.g. {@link org.bouncycastle.crypto.signers.BLSSigner#verifySignature
19+
* BLSSigner.verifySignature}) does not need to redo the ~128-bit
20+
* GLV-shortened subgroup-membership scalar multiplication on every call.
21+
* <p>
22+
* The validation cost is the same as a single verify call's previous
23+
* pre-pairing keyValidate step, paid once at construction instead of N
24+
* times for N verifies under the same key. Callers that deserialize
25+
* public keys from untrusted bytes typically run
26+
* {@link BLS12_381Serialization#decompressG1 decompressG1} immediately
27+
* before constructing this object; that decompression does not subgroup
28+
* check, so the constructor is the place where the prime-order check
29+
* happens.
1030
*/
1131
public class BLSPublicKeyParameters
1232
extends BLSKeyParameters
@@ -16,7 +36,21 @@ public class BLSPublicKeyParameters
1636
public BLSPublicKeyParameters(BLSParameters params, ECPoint publicPoint)
1737
{
1838
super(false, params);
19-
this.publicPoint = publicPoint.normalize();
39+
if (publicPoint == null)
40+
{
41+
throw new IllegalArgumentException("publicPoint must not be null");
42+
}
43+
ECPoint normalised = publicPoint.normalize();
44+
// KeyValidate per draft-irtf-cfrg-bls-signature sec. 2.5: rejects
45+
// identity, off-curve points (curve equation), and points in
46+
// E(Fp) \ G1 (subgroup check). This is the invariant the class
47+
// promises downstream — see the class javadoc.
48+
if (!BLS12_381BasicScheme.keyValidate(normalised))
49+
{
50+
throw new IllegalArgumentException(
51+
"invalid BLS public key: must be a non-identity point in the prime-order G1 subgroup");
52+
}
53+
this.publicPoint = normalised;
2054
}
2155

2256
/**

core/src/main/java/org/bouncycastle/crypto/signers/BLSSigner.java

Lines changed: 137 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package org.bouncycastle.crypto.signers;
22

3-
import java.io.ByteArrayOutputStream;
4-
53
import org.bouncycastle.crypto.CipherParameters;
64
import org.bouncycastle.crypto.CryptoException;
75
import org.bouncycastle.crypto.Signer;
@@ -118,7 +116,7 @@ public byte[] generateSignature()
118116
{
119117
throw new IllegalStateException("BLSSigner not initialised for signing");
120118
}
121-
byte[] msg = buffer.toByteArray();
119+
byte[] msg = buffer.snapshot();
122120
try
123121
{
124122
BLS12_381G2HashToCurve h = new BLS12_381G2HashToCurve(dst);
@@ -128,6 +126,11 @@ public byte[] generateSignature()
128126
}
129127
finally
130128
{
129+
// Wipe the snapshot copy in addition to the resetting the
130+
// backing buffer — hashToCurve has already read every byte
131+
// through the SHA-256 expand_message_xmd, so the array is
132+
// safe to zero at this point. See WipingBuffer's doc for
133+
// why the snapshot is needed at all.
131134
Arrays.fill(msg, (byte)0);
132135
reset();
133136
}
@@ -139,7 +142,6 @@ public boolean verifySignature(byte[] signature)
139142
{
140143
throw new IllegalStateException("BLSSigner not initialised for verification");
141144
}
142-
byte[] msg = buffer.toByteArray();
143145
try
144146
{
145147
BLS12_381G2Point sig;
@@ -151,28 +153,32 @@ public boolean verifySignature(byte[] signature)
151153
{
152154
return false;
153155
}
156+
154157
ECPoint pk = publicKey.getPublicPoint();
155-
if (!BLS12_381BasicScheme.keyValidate(pk))
158+
if (sig.isInfinity() || !BLS12_381SubgroupCheck.isInG2Subgroup(sig))
156159
{
157160
return false;
158161
}
159-
if (sig.isInfinity() || !BLS12_381SubgroupCheck.isInG2Subgroup(sig))
162+
byte[] msg = buffer.snapshot();
163+
try
160164
{
161-
return false;
165+
BLS12_381G2HashToCurve h = new BLS12_381G2HashToCurve(dst);
166+
BLS12_381G2Point q = h.hashToCurve(msg);
167+
ECCurve curve = BLS12_381G1.createCurve();
168+
ECPoint g1 = BLS12_381G1.getGenerator(curve);
169+
// e(g1, sig) == e(pk, H(msg)) <=> e(g1, sig) * e(-pk, H(msg)) == 1
170+
Fp12Element acc = BLS12_381Pairing.multiPair(
171+
new ECPoint[]{g1, pk.negate()},
172+
new BLS12_381G2Point[]{sig, q});
173+
return Fp12Element.ONE.equals(acc);
174+
}
175+
finally
176+
{
177+
Arrays.fill(msg, (byte)0);
162178
}
163-
BLS12_381G2HashToCurve h = new BLS12_381G2HashToCurve(dst);
164-
BLS12_381G2Point q = h.hashToCurve(msg);
165-
ECCurve curve = BLS12_381G1.createCurve();
166-
ECPoint g1 = BLS12_381G1.getGenerator(curve);
167-
// e(g1, sig) == e(pk, H(msg)) <=> e(g1, sig) * e(-pk, H(msg)) == 1
168-
Fp12Element acc = BLS12_381Pairing.multiPair(
169-
new ECPoint[]{g1, pk.negate()},
170-
new BLS12_381G2Point[]{sig, q});
171-
return Fp12Element.ONE.equals(acc);
172179
}
173180
finally
174181
{
175-
Arrays.fill(msg, (byte)0);
176182
reset();
177183
}
178184
}
@@ -183,23 +189,127 @@ public void reset()
183189
}
184190

185191
/**
186-
* ByteArrayOutputStream subclass that wipes its internal byte storage
187-
* before resetting the count, so message bytes don't linger in the
188-
* heap between {@code reset()} and the next GC.
192+
* Wipe-aware byte buffer backing the {@link Signer#update update}
193+
* contract — bytes accumulate here between {@link #init} /
194+
* {@link #reset} cycles.
195+
* <p>
196+
* <b>Wipe scope</b> (W3 in the review):
197+
* <ul>
198+
* <li>On {@link #wipeAndReset}: zero positions {@code 0..count} of
199+
* the current backing array, then reset {@code count}. Called
200+
* from the signer's {@code reset()}.</li>
201+
* <li>On internal capacity growth: zero the old backing array
202+
* before releasing it to GC, so the doubling-resize pattern
203+
* doesn't leave a chain of obsolete buffers each holding a
204+
* prefix of the message.</li>
205+
* <li>On {@link #snapshot}: returns a fresh defensive copy that
206+
* the signer's {@code finally} block zeroes after the
207+
* hash-to-curve consumes it. The copy is unavoidable because
208+
* {@code hashToCurve} takes a {@code byte[]}, not a
209+
* {@code (byte[], offset, length)} triple.</li>
210+
* </ul>
211+
* <p>
212+
* <b>What this does NOT cover.</b> The wipe is best-effort. Message
213+
* bytes still flow through the SHA-256 block buffer inside
214+
* {@code expand_message_xmd}; the JVM may relocate arrays during GC,
215+
* leaving spectral copies behind; and reflection / native debuggers
216+
* can observe live bytes anyway. The replacement of the previous
217+
* {@link java.io.ByteArrayOutputStream}-derived buffer was driven by
218+
* the doubling-resize gap above — the prior class overstated wipe
219+
* coverage in its docstring. In typical BLS use the message is not
220+
* secret (consensus signing roots, transaction bodies), so a strict
221+
* wipe is not load-bearing for the signer; this class minimises
222+
* residence as a defence-in-depth measure for callers who choose to
223+
* sign sensitive data.
189224
* <p>
190-
* Not thread-safe — {@link BLSSigner} itself follows the usual BC
191-
* convention of single-threaded use, so the {@code synchronized}
192-
* qualifiers on the inherited {@link ByteArrayOutputStream#write}
193-
* methods are not relied on, and {@code wipeAndReset} matches that
194-
* contract.
225+
* Not thread-safe — the BC {@link Signer} contract is per-instance,
226+
* per-thread.
195227
*/
196228
private static final class WipingBuffer
197-
extends ByteArrayOutputStream
198229
{
230+
private byte[] buf = new byte[64];
231+
private int count;
232+
233+
void write(byte b)
234+
{
235+
ensureCapacity(longSize(count, 1));
236+
buf[count++] = b;
237+
}
238+
239+
void write(byte[] in, int off, int len)
240+
{
241+
if (in == null)
242+
{
243+
throw new NullPointerException("input array must not be null");
244+
}
245+
if (off < 0 || len < 0 || ((long)off + (long)len) > (long)in.length)
246+
{
247+
throw new IndexOutOfBoundsException(
248+
"off=" + off + " len=" + len + " in.length=" + in.length);
249+
}
250+
ensureCapacity(longSize(count, len));
251+
System.arraycopy(in, off, buf, count, len);
252+
count += len;
253+
}
254+
255+
/**
256+
* @return a fresh copy of bytes {@code 0..count}. The caller is
257+
* responsible for wiping the returned array once consumed.
258+
*/
259+
byte[] snapshot()
260+
{
261+
byte[] out = new byte[count];
262+
System.arraycopy(buf, 0, out, 0, count);
263+
return out;
264+
}
265+
199266
void wipeAndReset()
200267
{
201268
Arrays.fill(buf, 0, count, (byte)0);
202-
this.count = 0;
269+
count = 0;
270+
}
271+
272+
/**
273+
* Compute {@code current + delta} as a {@code long} to detect
274+
* {@code int} overflow at the {@code count + len} boundary.
275+
* Casting through {@code long} avoids the silent wrap-around
276+
* that {@code int + int} would produce for ~2GB messages, which
277+
* would translate to negative-capacity arguments later.
278+
*/
279+
private static long longSize(int current, int delta)
280+
{
281+
return (long)current + (long)delta;
282+
}
283+
284+
private void ensureCapacity(long needed)
285+
{
286+
if (needed <= buf.length)
287+
{
288+
return;
289+
}
290+
if (needed > (long)(Integer.MAX_VALUE - 8))
291+
{
292+
throw new OutOfMemoryError(
293+
"BLSSigner buffer would exceed maximum array size");
294+
}
295+
int newCap = buf.length;
296+
while ((long)newCap < needed)
297+
{
298+
long doubled = (long)newCap << 1;
299+
if (doubled > (long)(Integer.MAX_VALUE - 8))
300+
{
301+
newCap = Integer.MAX_VALUE - 8;
302+
break;
303+
}
304+
newCap = (int)doubled;
305+
}
306+
byte[] grown = new byte[newCap];
307+
System.arraycopy(buf, 0, grown, 0, count);
308+
// Wipe BEFORE releasing the old buffer to GC: this is the
309+
// load-bearing part of W3. Without it, every doubling-grow
310+
// leaves another stale buffer in the heap.
311+
Arrays.fill(buf, 0, count, (byte)0);
312+
buf = grown;
203313
}
204314
}
205315
}

core/src/test/java/org/bouncycastle/crypto/hash2curve/test/BLS12_381BasicSchemeTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package org.bouncycastle.crypto.hash2curve.test;
22

33
import java.math.BigInteger;
4+
import java.security.SecureRandom;
45

56
import junit.framework.TestCase;
67
import org.bouncycastle.crypto.bls.BLS12_381BasicScheme;
78
import org.bouncycastle.crypto.bls.BLS12_381G1;
89
import org.bouncycastle.crypto.bls.BLS12_381G2Point;
10+
import org.bouncycastle.crypto.bls.BLS12_381SubgroupCheck;
911
import org.bouncycastle.math.ec.ECCurve;
1012
import org.bouncycastle.math.ec.ECPoint;
1113
import org.bouncycastle.util.Strings;
@@ -133,4 +135,76 @@ public void testVerifyRejectsInfinitySignature()
133135
assertFalse("infinity signature must not verify",
134136
BLS12_381BasicScheme.verify(pk, Strings.toUTF8ByteArray("any"), BLS12_381G2Point.INFINITY));
135137
}
138+
139+
// ---------------------------------------------------------------------
140+
// keyValidate negative-branch coverage (review gap G3).
141+
//
142+
// The existing testKeyValidateRejectsIdentity exercises the
143+
// non-identity guard. keyValidate also rejects null and off-subgroup
144+
// points; both should be pinned. (Off-curve rejection is harder to
145+
// exercise without bypassing ECCurve.createPoint's on-curve check;
146+
// skipped — the on-curve test path is implicitly covered by
147+
// testVerifyRejectsTamperedSignature, which constructs sig + G2_gen
148+
// and expects rejection.)
149+
// ---------------------------------------------------------------------
150+
151+
public void testKeyValidateRejectsNull()
152+
{
153+
assertFalse("null public key must be rejected",
154+
BLS12_381BasicScheme.keyValidate(null));
155+
}
156+
157+
public void testKeyValidateRejectsOffSubgroup()
158+
{
159+
// Construct a point on E(Fp) that is NOT in the prime-order G1
160+
// subgroup (same approach as
161+
// BLSKeyPairGeneratorTest.testPublicKeyParametersRejectsOffSubgroup).
162+
// A random curve point lands in G1 with probability ~1/cofactor
163+
// (cofactor ~2^126), so a handful of attempts is plenty.
164+
SecureRandom rng = new SecureRandom(new byte[]{91});
165+
BigInteger p = BLS12_381G1.Q;
166+
ECCurve curve = BLS12_381G1.createCurve();
167+
for (int attempt = 0; attempt < 16; ++attempt)
168+
{
169+
BigInteger x = new BigInteger(p.bitLength(), rng).mod(p);
170+
BigInteger rhs = x.modPow(BigInteger.valueOf(3), p)
171+
.add(BigInteger.valueOf(4)).mod(p);
172+
BigInteger y = rhs.modPow(p.add(BigInteger.ONE).shiftRight(2), p);
173+
if (!y.multiply(y).mod(p).equals(rhs))
174+
{
175+
continue;
176+
}
177+
ECPoint candidate = curve.createPoint(x, y);
178+
if (BLS12_381SubgroupCheck.isInG1Subgroup(candidate))
179+
{
180+
continue;
181+
}
182+
assertFalse("off-subgroup G1 point must fail keyValidate",
183+
BLS12_381BasicScheme.keyValidate(candidate));
184+
return;
185+
}
186+
fail("could not construct an off-subgroup G1 point in 16 attempts");
187+
}
188+
189+
// ---------------------------------------------------------------------
190+
// Empty-message round-trip (review gap G8).
191+
// Eth2 KAT vectors don't include a sign-and-verify with msg = [],
192+
// and the spec-level scheme tests above all use non-empty messages.
193+
// expand_message_xmd has explicit handling for empty inputs (the
194+
// length-prefix turns empty msg into a non-empty hash input); this
195+
// test pins the end-to-end behaviour.
196+
// ---------------------------------------------------------------------
197+
198+
public void testSignVerifyEmptyMessageRoundTrip()
199+
{
200+
BigInteger sk = BLS12_381BasicScheme.keyGen(ikm32(9), new byte[0]);
201+
ECPoint pk = BLS12_381BasicScheme.skToPk(sk);
202+
byte[] msg = new byte[0];
203+
BLS12_381G2Point sig = BLS12_381BasicScheme.sign(sk, msg);
204+
assertTrue("signature over empty message must verify",
205+
BLS12_381BasicScheme.verify(pk, msg, sig));
206+
// And must NOT verify under a non-empty distinguishing input.
207+
assertFalse("empty-msg signature must not verify under a different msg",
208+
BLS12_381BasicScheme.verify(pk, new byte[]{1}, sig));
209+
}
136210
}

0 commit comments

Comments
 (0)