• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  *   http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  * See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 package com.google.security.wycheproof;
15 
16 import static org.junit.Assert.assertTrue;
17 import static org.junit.Assert.fail;
18 
19 import com.google.security.wycheproof.WycheproofRunner.ProviderType;
20 import com.google.security.wycheproof.WycheproofRunner.SlowTest;
21 import java.lang.management.ManagementFactory;
22 import java.lang.management.ThreadMXBean;
23 import java.math.BigInteger;
24 import java.security.InvalidAlgorithmParameterException;
25 import java.security.KeyPair;
26 import java.security.KeyPairGenerator;
27 import java.security.MessageDigest;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.Signature;
30 import java.security.interfaces.ECPrivateKey;
31 import java.security.interfaces.ECPublicKey;
32 import java.security.spec.ECGenParameterSpec;
33 import java.security.spec.ECParameterSpec;
34 import java.util.Arrays;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.junit.runners.JUnit4;
38 
39 /**
40  * Tests ECDSA signatures.
41  *
42  * <p>Tests for signature verification with test vectors are in JsonSignatureTest.java toghether
43  * with other signature schemes.
44  *
45  * @author bleichen@google.com (Daniel Bleichenbacher)
46  */
47 @RunWith(JUnit4.class)
48 public class EcdsaTest {
49 
50   /**
51    * Determines the Hash name from the ECDSA algorithm. There is a small inconsistency in the naming
52    * of algorithms. The Oracle standard use no hyphen in SHA256WithECDSA but uses a hyphen in the
53    * message digest, i.e., SHA-256.
54    */
getHashAlgorithm(String ecdsaAlgorithm)55   private String getHashAlgorithm(String ecdsaAlgorithm) {
56     ecdsaAlgorithm = ecdsaAlgorithm.toUpperCase();
57     int idx = ecdsaAlgorithm.indexOf("WITH");
58     if (idx > 0) {
59       if (ecdsaAlgorithm.startsWith("SHA")) {
60         return "SHA-" + ecdsaAlgorithm.substring(3, idx);
61       } else {
62         return ecdsaAlgorithm.substring(0, idx);
63       }
64     }
65     return "";
66   }
67 
68   /**
69    * Extract the integer r from an ECDSA signature. This method implicitely assumes that the ECDSA
70    * signature is DER encoded. and that the order of the curve is smaller than 2^1024.
71    */
extractR(byte[] signature)72   BigInteger extractR(byte[] signature) throws Exception {
73     int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
74     int lengthR = signature[startR + 1];
75     return new BigInteger(Arrays.copyOfRange(signature, startR + 2, startR + 2 + lengthR));
76   }
77 
extractS(byte[] signature)78   BigInteger extractS(byte[] signature) throws Exception {
79     int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
80     int lengthR = signature[startR + 1];
81     int startS = startR + 2 + lengthR;
82     int lengthS = signature[startS + 1];
83     return new BigInteger(Arrays.copyOfRange(signature, startS + 2, startS + 2 + lengthS));
84   }
85 
86   /** Extract the k that was used to sign the signature. */
extractK(byte[] signature, BigInteger h, ECPrivateKey priv)87   BigInteger extractK(byte[] signature, BigInteger h, ECPrivateKey priv) throws Exception {
88     BigInteger x = priv.getS();
89     BigInteger n = priv.getParams().getOrder();
90     BigInteger r = extractR(signature);
91     BigInteger s = extractS(signature);
92     BigInteger k = x.multiply(r).add(h).multiply(s.modInverse(n)).mod(n);
93     return k;
94   }
95 
96   /**
97    * Computes the bias of samples as
98    *
99    * abs(sum(e^(2 pi i s m / modulus) for s in samples) / sqrt(samples.length).
100    *
101    * If the samples are taken from a uniform distribution in the range 0 .. modulus - 1
102    * and the number of samples is significantly larger than L^2
103    * then the probability that the result is larger than L is approximately e^(-L^2).
104    * The approximation can be derived from the assumption that samples taken from
105    * a uniform distribution give a result that approximates a standard complex normal
106    * distribution Z. I.e. Z has a density f_Z(z) = exp(-abs(z)^2) / pi.
107    * https://en.wikipedia.org/wiki/Complex_normal_distribution
108    */
bias(BigInteger[] samples, BigInteger modulus, BigInteger m)109   double bias(BigInteger[] samples, BigInteger modulus, BigInteger m) {
110     double sumReal = 0.0;
111     double sumImag = 0.0;
112     for (BigInteger s : samples) {
113       BigInteger r = s.multiply(m).mod(modulus);
114       // multiplier = 2 * pi / 2^52
115       double multiplier = 1.3951473992034527e-15;
116       // computes the quotent 2 * pi * r / modulus
117       double quot = r.shiftLeft(52).divide(modulus).doubleValue() * multiplier;
118       sumReal += Math.cos(quot);
119       sumImag += Math.sin(quot);
120     }
121     return Math.sqrt((sumReal * sumReal + sumImag * sumImag) / samples.length);
122   }
123 
124   /**
125    * This test checks the basic functionality of ECDSA. It simply tries to generate a key, sign and
126    * verify a message for a given, algorithm and curve.
127    *
128    * @param algorithm the algorithm to test (e.g. "SHA256WithECDSA")
129    * @param curve the curve to test (e.g. "secp256r1")
130    * @return whether the algorithm and curve are supported.
131    * @throws Exception if an unexpected error occurred.
132    */
testParameters(String algorithm, String curve)133   boolean testParameters(String algorithm, String curve) throws Exception {
134     String message = "123400";
135 
136     KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
137     ECGenParameterSpec ecSpec = new ECGenParameterSpec(curve);
138     KeyPair keyPair;
139     try {
140       keyGen.initialize(ecSpec);
141       keyPair = keyGen.generateKeyPair();
142     } catch (InvalidAlgorithmParameterException ex) {
143       // The curve is not supported.
144       // The documentation does not specify whether the method initialize
145       // has to reject unsupported curves or if only generateKeyPair checks
146       // whether the curve is supported.
147       return false;
148     }
149     ECPublicKey pub = (ECPublicKey) keyPair.getPublic();
150     ECPrivateKey priv = (ECPrivateKey) keyPair.getPrivate();
151 
152     // Print the parameters.
153     System.out.println("Parameters for curve:" + curve);
154     EcUtil.printParameters(pub.getParams());
155 
156     Signature signer;
157     Signature verifier;
158     try {
159       signer = Signature.getInstance(algorithm);
160       verifier = Signature.getInstance(algorithm);
161     } catch (NoSuchAlgorithmException ex) {
162       // The algorithm is not supported.
163       return false;
164     }
165     // Both algorithm and curve are supported.
166     // Hence, we expect that signing and verifying properly works.
167     byte[] messageBytes = message.getBytes("UTF-8");
168     signer.initSign(priv);
169     signer.update(messageBytes);
170     byte[] signature = signer.sign();
171     verifier.initVerify(pub);
172     verifier.update(messageBytes);
173     assertTrue(verifier.verify(signature));
174     return true;
175   }
176 
177   /**
178    * This test checks the basic functionality of ECDSA. This mainly checks that the provider follows
179    * the JCA interface.
180    */
181   @Test
testBasic()182   public void testBasic() throws Exception {
183     String algorithm = "SHA256WithECDSA";
184     String curve = "secp256r1";
185     assertTrue(testParameters(algorithm, curve));
186   }
187 
188   /** Checks whether the one time key k in ECDSA is biased. */
testBias(String algorithm, String curve, ECParameterSpec ecParams)189   public void testBias(String algorithm, String curve, ECParameterSpec ecParams) throws Exception {
190     KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
191     try {
192       keyGen.initialize(ecParams);
193     } catch (InvalidAlgorithmParameterException ex) {
194       System.out.println("This provider does not support curve:" + curve);
195       return;
196     }
197     KeyPair keyPair = keyGen.generateKeyPair();
198     ECPrivateKey priv = (ECPrivateKey) keyPair.getPrivate();
199     // If we throw a fair coin tests times then the probability that
200     // either heads or tails appears less than mincount is less than 2^{-32}.
201     // Therefore the test below is not expected to fail unless the generation
202     // of the one time keys is indeed biased.
203     final int tests = 1024;
204     final int mincount = 410;
205 
206     String hashAlgorithm = getHashAlgorithm(algorithm);
207     String message = "Hello";
208     byte[] messageBytes = message.getBytes("UTF-8");
209     byte[] digest = MessageDigest.getInstance(hashAlgorithm).digest(messageBytes);
210 
211     // TODO(bleichen): Truncate the digest if the digest size is larger than the
212     //   curve size.
213     BigInteger h = new BigInteger(1, digest);
214     BigInteger q = priv.getParams().getOrder();
215     BigInteger qHalf = q.shiftRight(1);
216 
217     Signature signer = Signature.getInstance(algorithm);
218     signer.initSign(priv);
219     BigInteger[] kList = new BigInteger[tests];
220     for (int i = 0; i < tests; i++) {
221       signer.update(messageBytes);
222       byte[] signature = signer.sign();
223       kList[i] = extractK(signature, h, priv);
224     }
225 
226     // Checks whether the most significant bits and the least significant bits
227     // of the value k are unbiased.
228     int countMsb = 0; // count the number of k's with lsb set
229     int countLsb = 0; // count the number of k's with msb set
230     for (BigInteger k : kList) {
231       if (k.testBit(0)) {
232         countLsb++;
233       }
234       if (k.compareTo(qHalf) > 0) {
235         countMsb++;
236       }
237     }
238     if (countLsb < mincount || countLsb > tests - mincount) {
239       fail("Bias detected in the least significant bit of k:" + countLsb);
240     }
241     if (countMsb < mincount || countMsb > tests - mincount) {
242       fail("Bias detected in the most significant bit of k:" + countMsb);
243     }
244 
245     // One situation where the bits above are not biased even if k itself is
246     // badly distributed is the case where the signer replaces s by
247     // min(s, q - s). Such a replacement is sometimes done to avoid signature
248     // malleability of ECDSA.
249     // Breitner and Heninger describe such cases in the paper
250     // "Biased Nonce Sense: Lattice Attacks against Weak ECDSA Signatures in Cryptocurrencies",
251     // https://eprint.iacr.org/2019/023.pdf
252     // The following tests should catch the bugs described in this paper.
253     // The threshold below has been chosen to give false positives with probability < 2^{-32}.
254     double threshold = 5;
255 
256     // This test detects for example the case when either k or q-k is small.
257     double bias1 = bias(kList, q, BigInteger.ONE);
258     if (bias1 > threshold) {
259       fail("Bias for k detected. bias1 = " + bias1);
260     }
261     // Same as above but shifing by one bit.
262     double bias2 = bias(kList, q, BigInteger.valueOf(2));
263     if (bias2 > threshold) {
264       fail("Bias for k detected. bias2 = " + bias2);
265     }
266     double bias3 = bias(kList, q, qHalf);
267     if (bias3 > threshold) {
268       fail("Bias for k detected. bias3 = " + bias3);
269     }
270     // Checks whether most significant bytes, words, dwords or qwords are strongly correlated.
271     for (int bits : new int[] {8, 16, 32, 64}) {
272       BigInteger multiplier = BigInteger.ONE.shiftLeft(bits).subtract(BigInteger.ONE);
273       double bias4 = bias(kList, q, multiplier);
274       if (bias4 > threshold) {
275         fail("Bias for k detected. bits = " + bits + " bias4 = " + bias4);
276       }
277     }
278   }
279 
280   @SlowTest(
281     providers = {
282       ProviderType.BOUNCY_CASTLE,
283       ProviderType.CONSCRYPT,
284       ProviderType.OPENJDK,
285       ProviderType.SPONGY_CASTLE
286     }
287   )
288   @Test
testBiasAll()289   public void testBiasAll() throws Exception {
290     testBias("SHA256WithECDSA", "secp256r1", EcUtil.getNistP256Params());
291     testBias("SHA224WithECDSA", "secp224r1", EcUtil.getNistP224Params());
292     testBias("SHA384WithECDSA", "secp384r1", EcUtil.getNistP384Params());
293     testBias("SHA512WithECDSA", "secp521r1", EcUtil.getNistP521Params());
294     testBias("SHA256WithECDSA", "brainpoolP256r1", EcUtil.getBrainpoolP256r1Params());
295   }
296 
297   /**
298    * Tests for a potential timing attack. This test checks if there is a correlation between the
299    * timing of signature generation and the size of the one-time key k. This is for example the case
300    * if a double and add method is used for the point multiplication. The test fails if such a
301    * correlation can be shown with high confidence. Further analysis will be necessary to determine
302    * how easy it is to exploit the bias in a timing attack.
303    */
304   // TODO(bleichen): Determine if there are exploitable providers.
305   //
306   // SunEC currently fails this test. Since ECDSA typically is used with EC groups whose order
307   // is 224 bits or larger, it is unclear whether the same attacks that apply to DSA are practical.
308   //
309   // The ECDSA implementation in BouncyCastle leaks information about k through timing too.
310   // The test has not been optimized to detect this bias. It would require about 5'000'000 samples,
311   // which is too much for a simple unit test.
312   //
313   // BouncyCastle uses FixedPointCombMultiplier for ECDSA. This is a method using
314   // precomputation. The implementation is not constant time, since the precomputation table
315   // contains the point at infinity and adding this point is faster than ordinary point additions.
316   // The timing leak only has a small correlation to the size of k and at the moment it is is very
317   // unclear if the can be exploited. (Randomizing the precomputation table by adding the same
318   // random point to each element in the table and precomputing the necessary offset to undo the
319   // precomputation seems much easier than analyzing this.)
testTiming(String algorithm, String curve, ECParameterSpec ecParams)320   public void testTiming(String algorithm, String curve, ECParameterSpec ecParams)
321       throws Exception {
322     ThreadMXBean bean = ManagementFactory.getThreadMXBean();
323     if (!bean.isCurrentThreadCpuTimeSupported()) {
324       System.out.println("getCurrentThreadCpuTime is not supported. Skipping");
325       return;
326     }
327     KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
328     try {
329       keyGen.initialize(ecParams);
330     } catch (InvalidAlgorithmParameterException ex) {
331       System.out.println("This provider does not support curve:" + curve);
332       return;
333     }
334     KeyPair keyPair = keyGen.generateKeyPair();
335     ECPrivateKey priv = (ECPrivateKey) keyPair.getPrivate();
336 
337     String message = "Hello";
338     String hashAlgorithm = getHashAlgorithm(algorithm);
339     byte[] messageBytes = message.getBytes("UTF-8");
340     byte[] digest = MessageDigest.getInstance(hashAlgorithm).digest(messageBytes);
341     BigInteger h = new BigInteger(1, digest);
342     Signature signer = Signature.getInstance(algorithm);
343     signer.initSign(priv);
344     // The number of samples used for the test. This number is a bit low.
345     // I.e. it just barely detects that SunEC leaks information about the size of k.
346     int samples = 50000;
347     long[] timing = new long[samples];
348     BigInteger[] k = new BigInteger[samples];
349     for (int i = 0; i < samples; i++) {
350       long start = bean.getCurrentThreadCpuTime();
351       signer.update(messageBytes);
352       byte[] signature = signer.sign();
353       timing[i] = bean.getCurrentThreadCpuTime() - start;
354       k[i] = extractK(signature, h, priv);
355     }
356     long[] sorted = Arrays.copyOf(timing, timing.length);
357     Arrays.sort(sorted);
358     double n = priv.getParams().getOrder().doubleValue();
359     double expectedAverage = n / 2;
360     double maxSigma = 0;
361     System.out.println("testTiming algorithm:" + algorithm);
362     for (int idx = samples - 1; idx > 10; idx /= 2) {
363       long cutoff = sorted[idx];
364       int count = 0;
365       BigInteger total = BigInteger.ZERO;
366       for (int i = 0; i < samples; i++) {
367         if (timing[i] <= cutoff) {
368           total = total.add(k[i]);
369           count += 1;
370         }
371       }
372       double expectedStdDev = n / Math.sqrt(12 * count);
373       double average = total.doubleValue() / count;
374       // Number of standard deviations that the average is away from
375       // the expected value:
376       double sigmas = Math.abs(expectedAverage - average) / expectedStdDev;
377       if (sigmas > maxSigma) {
378         maxSigma = sigmas;
379       }
380       System.out.println(
381           "count:"
382               + count
383               + " cutoff:"
384               + cutoff
385               + " relative average:"
386               + (average / expectedAverage)
387               + " sigmas:"
388               + sigmas);
389     }
390     // Checks if the signatures with a small timing have a biased k.
391     // We use 7 standard deviations, so that the probability of a false positive is smaller
392     // than 10^{-10}.
393     if (maxSigma >= 7) {
394       fail("Signatures with short timing have a biased k");
395     }
396   }
397 
398   @SlowTest(
399     providers = {
400       ProviderType.BOUNCY_CASTLE,
401       ProviderType.CONSCRYPT,
402       ProviderType.OPENJDK,
403       ProviderType.SPONGY_CASTLE
404     }
405   )
406   @Test
testTimingAll()407   public void testTimingAll() throws Exception {
408     testTiming("SHA256WithECDSA", "secp256r1", EcUtil.getNistP256Params());
409     // TODO(bleichen): crypto libraries sometimes use optimized code for curves that are frequently
410     //   used. Hence it would make sense to test distinct curves. But at the moment testing many
411     //   curves is not practical since one test alone is already quite time consuming.
412     // testTiming("SHA224WithECDSA", "secp224r1", EcUtil.getNistP224Params());
413     // testTiming("SHA384WithECDSA", "secp384r1", EcUtil.getNistP384Params());
414     // testTiming("SHA512WithECDSA", "secp521r1", EcUtil.getNistP521Params());
415     // testTiming("SHA256WithECDSA", "brainpoolP256r1", EcUtil.getBrainpoolP256r1Params());
416   }
417 }
418