From 216ed9bdc15e4119ad89be24de8a752fdf950bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mamy=20Andr=C3=A9-Ratsimbazafy?= Date: Sun, 9 Feb 2020 22:01:01 +0100 Subject: [PATCH] Property-based testing framework vs GMP --- constantine/bigints.nim | 4 +- tests/test_bigints_vs_gmp.nim | 138 ++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/test_bigints_vs_gmp.nim diff --git a/constantine/bigints.nim b/constantine/bigints.nim index 9bcb5a0..5823869 100644 --- a/constantine/bigints.nim +++ b/constantine/bigints.nim @@ -131,8 +131,8 @@ func setZero*(a: var BigInt, start, stop: static int) {.inline.} = ## The [start, stop] range is inclusive ## If stop < start, a is unmodified static: - doAssert start in 0 ..< a.limbs.len - doAssert stop in 0 ..< a.limbs.len + doAssert start in 0 ..< a.limbs.len, $start & " not in 0 ..< " & $a.limbs.len & " (numLimbs)" + doAssert stop in 0 ..< a.limbs.len, $stop & " not in 0 ..< " & $a.limbs.len & " (numLimbs)" for i in static(start .. stop): a.limbs[i] = Zero diff --git a/tests/test_bigints_vs_gmp.nim b/tests/test_bigints_vs_gmp.nim new file mode 100644 index 0000000..4015c47 --- /dev/null +++ b/tests/test_bigints_vs_gmp.nim @@ -0,0 +1,138 @@ +# Constantine +# Copyright (c) 2018-2019 Status Research & Development GmbH +# Copyright (c) 2020-Present Mamy André-Ratsimbazafy +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import + # Standard library + random, macros, times, strutils, + # Third-party + gmp, stew/byteutils, + # Internal + ../constantine/[io, bigints, primitives] + +# We test up to 1024-bit, more is really slow + +var bitSizeRNG {.compileTime.} = initRand(1234) +const CryptoModSizes = [ + # Modulus sizes occuring in crypto + # To be tested more often + + # RSA + 1024, + # secp256k1, Curve25519 + 256, + # Barreto-Naehrig + 254, # BN254 + # Barreto-Lynn-Scott + 381, # BLS12-381 + 383, # BLS12-383 + 461, # BLS12-461 + 480, # BLS24-480 + # NIST recommended curves for US Federal Government (FIPS) + # https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + 192, + 224, + # 256 + 384, + 521 +] + +macro testRandomModSizes(numSizes: static int, aBits, mBits, body: untyped): untyped = + ## Generate `numSizes` random bit sizes known at compile-time to test against GMP + ## for A mod M + result = newStmtList() + + for _ in 0 ..< numSizes: + let aBitsVal = bitSizeRNG.rand(126 .. 4096) + let mBitsVal = block: + # Pick from curve modulus if odd + if bool(bitSizeRNG.rand(high(int)) and 1): + bitSizeRNG.sample(CryptoModSizes) + else: + # range 62..1024 to highlight edge effects of the WordBitSize (63) + bitSizeRNG.rand(62 .. 1024) + + result.add quote do: + block: + const `aBits` = `aBitsVal` + const `mBits` = `mBitsVal` + block: + `body` + +const # https://gmplib.org/manual/Integer-Import-and-Export.html + GMP_WordLittleEndian = -1'i32 + GMP_WordNativeEndian = 0'i32 + GMP_WordBigEndian = 1'i32 + + GMP_MostSignificantWordFirst = 1'i32 + GMP_LeastSignificantWordFirst = -1'i32 + +proc main() = + var gmpRng: gmp_randstate_t + gmp_randinit_mt(gmpRng) + # The GMP seed varies between run so that + # test coverage increases as the library gets tested. + # This requires to dump the seed in the console or the function inputs + # to be able to reproduce a bug + let seed = uint32(getTime().toUnix() and (1'i64 shl 32 - 1)) # unixTime mod 2^32 + echo "GMP seed: ", seed + gmp_randseed_ui(gmpRng, seed) + + var a, m, r: mpz_t + mpz_init(a) + mpz_init(m) + mpz_init(r) + + testRandomModSizes(100, aBits, mBits): + echo "Testing: Dividend bitsize " & align($aBits, 4) & " -- modulus bitsize " & align($mBits, 4) + + # Generate random value in the range 0 ..< 2^aBits + mpz_urandomb(a, gmpRng, aBits) + # Generate random modulus and ensure the MSB is set + mpz_urandomb(m, gmpRng, mBits) + mpz_setbit(m, mBits-1) + + ######################################################### + # Conversion buffers + const aLen = (aBits + 7) div 8 + const mLen = (mBits + 7) div 8 + + var aBuf: array[aLen, byte] + var mBuf: array[mLen, byte] + + var aW, mW: csize # Word written by GMP + + discard mpz_export(aBuf[0].addr, aW.addr, GMP_LeastSignificantWordFirst, 1, GMP_WordNativeEndian, 0, a) + discard mpz_export(mBuf[0].addr, mW.addr, GMP_LeastSignificantWordFirst, 1, GMP_WordNativeEndian, 0, m) + + # Since the modulus is using all bits, it's we can test for exact amount copy + doAssert aLen >= aW, "Expected at most " & $aLen & " bytes but wrote " & $aW & " for " & toHex(aBuf) & " (little-endian)" + doAssert mLen == mW, "Expected " & $mLen & " bytes but wrote " & $mW & " for " & toHex(mBuf) & " (little-endian)" + + # Build the bigint + let aTest = BigInt[aBits].fromRawUint(aBuf, littleEndian) + let mTest = BigInt[mBits].fromRawUint(mBuf, littleEndian) + + ######################################################### + # Modulus + mpz_mod(r, a, m) + + var rTest: BigInt[mBits] + rTest.reduce(aTest, mTest) + + ######################################################### + # Check + var rGMP: array[mLen, byte] + var rW: csize # Word written by GMP + discard mpz_export(rGMP[0].addr, rW.addr, GMP_LeastSignificantWordFirst, 1, GMP_WordNativeEndian, 0, r) + + var rConstantine: array[mLen, byte] + dumpRawUint(rConstantine, rTest, littleEndian) + + doAssert rGMP == rConstantine + +main()