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