11package org .bouncycastle .crypto .signers ;
22
3- import java .io .ByteArrayOutputStream ;
4-
53import org .bouncycastle .crypto .CipherParameters ;
64import org .bouncycastle .crypto .CryptoException ;
75import 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}
0 commit comments