From 2b6dc959702a089d6c37a2b61e77bb59dadb7477 Mon Sep 17 00:00:00 2001 From: MMGen Date: Fri, 15 Mar 2019 09:09:38 +0000 Subject: [PATCH] rewrite SHA2 implementation, support SHA512; update tests accordingly - Note: the sha2 module is used by MMGen solely for generating Zcash addresses and viewkeys --- mmgen/addr.py | 2 +- mmgen/sha2.py | 200 ++++++++++++++++++++++++++++++++++++++++ mmgen/sha256.py | 142 ---------------------------- scripts/test-release.sh | 48 +++++----- setup.py | 2 +- test/sha256test.py | 65 ------------- test/sha2test.py | 127 +++++++++++++++++++++++++ 7 files changed, 354 insertions(+), 232 deletions(-) create mode 100755 mmgen/sha2.py delete mode 100755 mmgen/sha256.py delete mode 100755 test/sha256test.py create mode 100755 test/sha2test.py diff --git a/mmgen/addr.py b/mmgen/addr.py index f33b2d02..4dbb9aae 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -101,7 +101,7 @@ class AddrGeneratorZcashZ(AddrGenerator): s = bytearray(s + bytes(32)) s[0] |= 0xc0 s[32] = t - from mmgen.sha256 import Sha256 + from mmgen.sha2 import Sha256 return Sha256(s,preprocess=False).digest() def to_addr(self,pubhex): # pubhex is really privhex diff --git a/mmgen/sha2.py b/mmgen/sha2.py new file mode 100755 index 00000000..4c6fa636 --- /dev/null +++ b/mmgen/sha2.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +sha2.py: A non-optimized but very compact implementation of the SHA2 hash + algorithm for the MMGen suite. Implements SHA256, SHA512 and + SHA256Compress (unpadded SHA256, required for Zcash addresses) +""" + +from binascii import hexlify +from struct import pack,unpack + +class Sha2(object): + 'Implementation based on the pseudocode at https://en.wikipedia.org/wiki/SHA-2' + K = None + + @classmethod + def initConstants(cls): + + from math import sqrt + + def nextPrime(n=2): + while True: + for factor in range(2,int(sqrt(n))+1): + if n % factor == 0: + break + else: + yield n + n += 1 + + # Find the first nRounds primes + npgen = nextPrime() + primes = [next(npgen) for x in range(cls.nRounds)] + + fb_mul = 2 ** cls.wordBits + def getFractionalBits(n): + return int((n - int(n)) * fb_mul) + + if cls.use_gmp: + from gmpy2 import context,set_context,sqrt,cbrt + set_context(context(precision=75)) + else: + cbrt = lambda n: pow(n, 1 / 3) + + # First wordBits bits of the fractional parts of the square roots of the first 8 primes + H = (getFractionalBits(sqrt(n)) for n in primes[:8]) + + # First wordBits bits of the fractional parts of the cube roots of the first nRounds primes + K = (getFractionalBits(cbrt(n)) for n in primes) + + cls.H_init = tuple(H) + cls.K = tuple(K) + + def __init__(self,message,preprocess=True): + 'Use preprocess=False for Sha256Compress' + assert type(message) in (bytes,bytearray,list),'message must be of type bytes, bytearray or list' + if self.K == None: + type(self).initConstants() + self.H = list(self.H_init) + self.M = message + self.W = [0] * self.nRounds + if preprocess: + self.padMessage() + self.bytesToWords() + self.compute() + + def padMessage(self): + """ + Pre-processing (padding) (adapted from the standard, translating bits to bytes) + - Begin with the original message of length MSGLEN bytes + - Create MLPACK := MSGLEN * 8 encoded as a blkSize-bit big-endian integer + - Append 0x80 (0b10000000) + - Append PADLEN null bytes, where PADLEN is the minimum number >= 0 such that + MSGLEN + 1 + PADLEN + len(MLPACK) is a multiple of blkSize + - Append MLPACK + """ + mlpack = self.pack_msglen() + padlen = self.blkSize - (len(self.M) % self.blkSize) - len(mlpack) - 1 + if padlen < 0: padlen += self.blkSize + self.M = self.M + bytes([0x80] + [0] * padlen) + mlpack + + def bytesToWords(self): + ws = self.wordSize + assert len(self.M) % ws == 0 + self.M = tuple([unpack(self.word_fmt,self.M[i*ws:ws+(i*ws)])[0] for i in range(len(self.M) // ws)]) + + def digest(self): + return b''.join((pack(self.word_fmt,w) for w in self.H)) + + def hexdigest(self): + return hexlify(self.digest()) + + def compute(self): + for i in range(0,len(self.M),16): + self.processBlock(i) + + def processBlock(self,offset): + 'Process a blkSize-byte chunk of the message' + + def rrotate(a,b): return ((a << self.wordBits-b) & self.wordMask) | (a >> b) + def addm(a,b): return (a + b) & self.wordMask + + # Copy chunk into first 16 words of message schedule array + for i in range(16): + self.W[i] = self.M[offset + i] + + # Extend the first 16 words into the remaining nRounds words of message schedule array + for i in range(16,self.nRounds): + g0 = self.W[i-15] + gamma0 = rrotate(g0,self.g0r1) ^ rrotate(g0,self.g0r2) ^ (g0 >> self.g0r3) + g1 = self.W[i-2] + gamma1 = rrotate(g1,self.g1r1) ^ rrotate(g1,self.g1r2) ^ (g1 >> self.g1r3) + self.W[i] = addm(addm(addm(gamma0,self.W[i-7]),gamma1),self.W[i-16]) + + # Initialize working variables from current hash state + a,b,c,d,e,f,g,h = self.H + + # Compression function main loop + for i in range(self.nRounds): + ch = (e & f) ^ (~e & g) + maj = (a & b) ^ (a & c) ^ (b & c) + + sigma0 = rrotate(a,self.s0r1) ^ rrotate(a,self.s0r2) ^ rrotate(a,self.s0r3) + sigma1 = rrotate(e,self.s1r1) ^ rrotate(e,self.s1r2) ^ rrotate(e,self.s1r3) + + t1 = addm(addm(addm(addm(h,sigma1),ch),self.K[i]),self.W[i]) + t2 = addm(sigma0,maj) + + h = g + g = f + f = e + e = addm(d,t1) + d = c + c = b + b = a + a = addm(t1,t2) + + # Save hash state + for n,v in enumerate((a,b,c,d,e,f,g,h)): + self.H[n] = addm(self.H[n],v) + +class Sha256(Sha2): + use_gmp = False + g0r1,g0r2,g0r3 = (7,18,3) + g1r1,g1r2,g1r3 = (17,19,10) + s0r1,s0r2,s0r3 = (2,13,22) + s1r1,s1r2,s1r3 = (6,11,25) + blkSize = 64 + nRounds = 64 + wordSize = 4 + wordBits = 32 + wordMask = 0xffffffff + word_fmt = '>I' + + def pack_msglen(self): + return pack('>Q',len(self.M)*8) + +class Sha512(Sha2): + """ + SHA-512 is identical in structure to SHA-256, but: + - the message is broken into 1024-bit chunks, + - the initial hash values and round constants are extended to 64 bits, + - there are 80 rounds instead of 64, + - the message schedule array W has 80 64-bit words instead of 64 32-bit words, + - to extend the message schedule array W, the loop is from 16 to 79 instead of from 16 to 63, + - the round constants are based on the first 80 primes 2..409, + - the word size used for calculations is 64 bits long, + - the appended length of the message (before pre-processing), in bits, is a 128-bit big-endian integer, and + - the shift and rotate amounts used are different. + """ + use_gmp = True + g0r1,g0r2,g0r3 = (1,8,7) + g1r1,g1r2,g1r3 = (19,61,6) + s0r1,s0r2,s0r3 = (28,34,39) + s1r1,s1r2,s1r3 = (14,18,41) + blkSize = 128 + nRounds = 80 + wordSize = 8 + wordBits = 64 + wordMask = 0xffffffffffffffff + word_fmt = '>Q' + + def pack_msglen(self): + return pack('>Q', (len(self.M)*8) // (2**64)) + \ + pack('>Q', (len(self.M)*8) % (2**64)) diff --git a/mmgen/sha256.py b/mmgen/sha256.py deleted file mode 100755 index 15884564..00000000 --- a/mmgen/sha256.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -# -# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution -# Copyright (C)2013-2019 The MMGen Project -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -sha256.py: custom sha256 implementation for Zcash -""" - -# Sha256 code ported from JavaScript, originally here: -# CryptoJS v3.1.2 code.google.com/p/crypto-js -# (c) 2009-2013 by Jeff Mott. All rights reserved. -# code.google.com/p/crypto-js/wiki/License -# via here: -# https://www.npmjs.com/package/sha256 -# and here: -# https://github.com/howardwu/zaddr - -from binascii import hexlify - -class Sha256(object): - - def initConstants(): - import math - def isPrime(n): - for factor in range(2,int(math.sqrt(n))+1): - if not (n % factor): return False - return True - - def getFractionalBits(n): - return int((n - int(n)) * 0x100000000) - - k = [0] * 64 - n,nPrime = 2,0 - while nPrime < 64: - if isPrime(n): - k[nPrime] = getFractionalBits(math.pow(n, 1 / 3)) - nPrime += 1 - n += 1 - - return k - - K = initConstants() - - # preprocess: True=Sha256(), False=Sha256Compress() - def __init__(self,message,preprocess=True): - self.H = [0x6A09E667,0xBB67AE85,0x3C6EF372,0xA54FF53A,0x510E527F,0x9B05688C,0x1F83D9AB,0x5BE0CD19] - self.M = message - self.W = [0] * 64 - (self.bytesToWords,self.preprocessBlock)[preprocess]() -# self.initConstants() - self.compute() - - def digest(self): - self.M = self.H - self.wordsToBytes() - return self.M - - def hexdigest(self): - return hexlify(self.digest()) - - def bytesToWords(self): - assert type(self.M) in (bytes,bytearray,list) - words = [0] * (len(self.M) // 4 + len(self.M) % 4) - b = 0 - for i in range(len(self.M)): - words[b >> 5] |= self.M[i] << (24 - b % 32) - b += 8 - self.M = words - - def wordsToBytes(self): - assert type(self.M) == list and len(self.M) == 8 - self.M = bytes([(self.M[b >> 5] >> (24 - b % 32) & 0xff) for b in range(0,len(self.M)*32,8)]) - - def preprocessBlock(self): - def lshift(a,b): return (a << b) & 0xffffffff - assert type(self.M) in (bytes,bytearray) - l = len(self.M) * 8 - self.bytesToWords() - last_idx = lshift((l + 64 >> 9),4) + 15 - self.M.extend([0] * (last_idx - len(self.M) + 1)) - self.M[l >> 5] |= lshift(0x80, (24 - l % 32)) - self.M[last_idx] = l - - def compute(self): - assert type(self.M) == list - for i in range(0,len(self.M),16): - self.processBlock(i) - - def processBlock(self,offset): - def lshift(a,b): return (a << b) & 0xffffffff - def sumr(a,b): return (a + b) & 0xffffffff - a,b,c,d,e,f,g,h = [self.H[i] for i in range(8)] # Working variables - - for i in range(64): - if i < 16: - self.W[i] = self.M[offset + i] - else: - gamma0x = self.W[i - 15] - gamma0 = ( (lshift(gamma0x,25) | (gamma0x >> 7)) ^ - (lshift(gamma0x,14) | (gamma0x >> 18)) ^ - (gamma0x >> 3) ) - gamma1x = self.W[i - 2] - gamma1 = ( (lshift(gamma1x,15) | (gamma1x >> 17)) ^ - (lshift(gamma1x,13) | (gamma1x >> 19)) ^ - (gamma1x >> 10) ) - self.W[i] = sumr(sumr(sumr(gamma0,self.W[i - 7]),gamma1),self.W[i - 16]) - - ch = (e & f) ^ (~e & g) - maj = (a & b) ^ (a & c) ^ (b & c) - - sigma0 = (lshift(a,30) | (a >> 2)) ^ (lshift(a,19) | (a >> 13)) ^ (lshift(a,10) | (a >> 22)) - sigma1 = (lshift(e,26) | (e >> 6)) ^ (lshift(e,21) | (e >> 11)) ^ (lshift(e,7) | (e >> 25)) - - t1 = sumr(sumr(sumr(sumr(h,sigma1),ch),self.K[i]),self.W[i]) - t2 = sumr(sigma0,maj) - - h = g - g = f - f = e - e = sumr(d,t1) - d = c - c = b - b = a - a = sumr(t1,t2) - - # Intermediate hash value - for n,v in enumerate([a,b,c,d,e,f,g,h]): - self.H[n] = sumr(self.H[n],v) diff --git a/scripts/test-release.sh b/scripts/test-release.sh index 4fc09464..cf4ccb88 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -18,10 +18,10 @@ scrambletest_py='test/scrambletest.py' mmgen_tool='cmds/mmgen-tool' mmgen_keygen='cmds/mmgen-keygen' python='python3' -rounds=100 rounds_low=20 rounds_spec=500 sha_rounds=500 gen_rounds=10 +rounds=100 rounds_mid=250 rounds_max=500 monero_addrs='3,99,2,22-24,101-104' -dfl_tests='obj sha256 alts monero eth autosign btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool tool2 gen' +dfl_tests='obj sha2 alts monero eth autosign btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool tool2 gen' add_tests='autosign_minimal autosign_live' PROGNAME=$(basename $0) @@ -45,7 +45,7 @@ do echo " '-V' Run test/test.py and other commands with '--verbose' switch" echo " AVAILABLE TESTS:" echo " obj - data objects" - echo " sha256 - MMGen sha256 implementation" + echo " sha2 - MMGen sha2 implementation" echo " alts - operations for all supported gen-only altcoins" echo " monero - operations for Monero" echo " eth - operations for Ethereum" @@ -75,7 +75,7 @@ do gentest_py="$python $gentest_py" mmgen_tool="$python $mmgen_tool" mmgen_keygen="$python $mmgen_keygen" ;& - f) rounds=2 rounds_low=2 rounds_spec=10 sha_rounds=40 gen_rounds=2 monero_addrs='3,23' ;; + f) rounds=10 rounds_mid=25 rounds_max=50 monero_addrs='3,23' ;; i) INSTALL=1 ;; I) INSTALL_ONLY=1 ;; l) echo -e "Default tests:\n $dfl_tests" @@ -162,10 +162,12 @@ t_obj=( "$objtest_py --coin=ltc --testnet=1") f_obj='Data object test complete' -i_sha256='MMGen sha256 implementation' -s_sha256='Testing sha256 implementation' -t_sha256=("$python test/sha256test.py $sha_rounds") -f_sha256='Sha256 test complete' +i_sha2='MMGen SHA2 implementation' +s_sha2='Testing SHA2 implementation' +t_sha2=( + "$python test/sha2test.py sha256 $rounds_max" + "$python test/sha2test.py sha512 $rounds_max") +f_sha2='SHA2 test complete' i_alts='Gen-only altcoin' s_alts='The following tests will test generation operations for all supported altcoins' @@ -183,7 +185,7 @@ t_alts=( "$gentest_py --coin=etc 2 $rounds" "$gentest_py --coin=eth 2 $rounds" "$gentest_py --coin=zec 2 $rounds" - "$gentest_py --coin=zec --type=zcash_z 2 $rounds_spec" + "$gentest_py --coin=zec --type=zcash_z 2 $rounds_mid" "$gentest_py --coin=btc 2:ext $rounds" "$gentest_py --coin=btc --type=compressed 2:ext $rounds" @@ -194,12 +196,12 @@ t_alts=( "$gentest_py --coin=etc 2:ext $rounds" "$gentest_py --coin=eth 2:ext $rounds" "$gentest_py --coin=zec 2:ext $rounds" - "$gentest_py --coin=zec --type=zcash_z 2:ext $rounds_spec" + "$gentest_py --coin=zec --type=zcash_z 2:ext $rounds_mid" - "$gentest_py --all 2:pycoin $rounds_low" - "$gentest_py --all 2:pyethereum $rounds_low" - "$gentest_py --all 2:keyconv $rounds_low" - "$gentest_py --all 2:zcash_mini $rounds_low") + "$gentest_py --all 2:pycoin $rounds" + "$gentest_py --all 2:pyethereum $rounds" + "$gentest_py --all 2:keyconv $rounds_mid" + "$gentest_py --all 2:zcash_mini $rounds_mid") if [ "$MINGW" ]; then t_alts[13]="# MSWin platform: skipping zcash z-addr generation and altcoin verification with third-party tools" i=14 end=${#t_alts[*]} @@ -374,20 +376,20 @@ t_gen=( "$gentest_py -q 2 $REFDIR/btcwallet.dump" "$gentest_py -q --type=segwit 2 $REFDIR/btcwallet-segwit.dump" "$gentest_py -q --type=bech32 2 $REFDIR/btcwallet-bech32.dump" - "$gentest_py -q 1:2 $gen_rounds" - "$gentest_py -q --type=segwit 1:2 $gen_rounds" - "$gentest_py -q --type=bech32 1:2 $gen_rounds" + "$gentest_py -q 1:2 $rounds" + "$gentest_py -q --type=segwit 1:2 $rounds" + "$gentest_py -q --type=bech32 1:2 $rounds" "$gentest_py -q --testnet=1 2 $REFDIR/btcwallet-testnet.dump" - "$gentest_py -q --testnet=1 1:2 $gen_rounds" - "$gentest_py -q --testnet=1 --type=segwit 1:2 $gen_rounds" + "$gentest_py -q --testnet=1 1:2 $rounds" + "$gentest_py -q --testnet=1 --type=segwit 1:2 $rounds" "$gentest_py -q --coin=ltc 2 $REFDIR/litecoin/ltcwallet.dump" "$gentest_py -q --coin=ltc --type=segwit 2 $REFDIR/litecoin/ltcwallet-segwit.dump" "$gentest_py -q --coin=ltc --type=bech32 2 $REFDIR/litecoin/ltcwallet-bech32.dump" - "$gentest_py -q --coin=ltc 1:2 $gen_rounds" - "$gentest_py -q --coin=ltc --type=segwit 1:2 $gen_rounds" + "$gentest_py -q --coin=ltc 1:2 $rounds" + "$gentest_py -q --coin=ltc --type=segwit 1:2 $rounds" "$gentest_py -q --coin=ltc --testnet=1 2 $REFDIR/litecoin/ltcwallet-testnet.dump" - "$gentest_py -q --coin=ltc --testnet=1 1:2 $gen_rounds" - "$gentest_py -q --coin=ltc --testnet=1 --type=segwit 1:2 $gen_rounds") + "$gentest_py -q --coin=ltc --testnet=1 1:2 $rounds" + "$gentest_py -q --coin=ltc --testnet=1 --type=segwit 1:2 $rounds") f_gen='gentest tests completed' [ -d .git -a -n "$INSTALL" -a -z "$TESTING" ] && { diff --git a/setup.py b/setup.py index 55d2518b..a6b4555f 100755 --- a/setup.py +++ b/setup.py @@ -136,7 +136,7 @@ setup( 'mmgen.regtest', 'mmgen.rpc', 'mmgen.seed', - 'mmgen.sha256', + 'mmgen.sha2', 'mmgen.term', 'mmgen.tool', 'mmgen.tw', diff --git a/test/sha256test.py b/test/sha256test.py deleted file mode 100755 index 9dc071e5..00000000 --- a/test/sha256test.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -# -# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution -# Copyright (C)2013-2019 The MMGen Project -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import sys,os,hashlib -from binascii import hexlify -from mmgen.sha256 import Sha256 - -random_rounds = int(sys.argv[1]) if len(sys.argv) == 2 else 500 - -def msg(s): sys.stderr.write(s) -def green(s): return '\033[32;1m' + s + '\033[0m' - -def compare_hashes(dlen,data): - sha2 = hashlib.sha256(data).hexdigest().encode() -# msg('Dlen {:<5} {}\r'.format(dlen,sha2)) - my_sha2 = Sha256(data).hexdigest() - assert my_sha2 == sha2,'Hashes do not match!' - -def test_K(): - msg('Testing generated constants: ') - K_ref = [1116352408,1899447441,-1245643825,-373957723,961987163,1508970993,-1841331548,-1424204075,-670586216,310598401,607225278,1426881987,1925078388,-2132889090,-1680079193,-1046744716,-459576895,-272742522,264347078,604807628,770255983,1249150122,1555081692,1996064986,-1740746414,-1473132947,-1341970488,-1084653625,-958395405,-710438585,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,-2117940946,-1838011259,-1564481375,-1474664885,-1035236496,-949202525,-778901479,-694614492,-200395387,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,-2067236844,-1933114872,-1866530822,-1538233109,-1090935817,-965641998] - def toSigned32(n): return ((n & 0xffffffff) ^ 0x80000000) - 0x80000000 - K_sig = [toSigned32(n) for n in Sha256.K] - assert K_sig == K_ref,'Generated constants in K[] differ from reference value' - msg('OK\n') - -def test_ref(): - inputs = ( - '','x','xa','the','the quick','the quick brown fox', - '\x00','\x00\x00','\x00'*256,'\x00'*512,'\x00'*511,'\x00'*513, - '\x0f','\x0f\x0f','\x0f'*256,'\x0f'*512,'\x0f'*511,'\x0f'*513, - '\x0f\x0d','\x0e\x0e'*256,'\x00\x0f'*512,'\x0e\x0f'*511,'\x0a\x0d'*513 - ) - for i,data in enumerate(inputs): - msg('\rTesting reference input data: {:4}/{} '.format(i+1,len(inputs))) - compare_hashes(len(data),data.encode()) - msg('OK\n') - -def test_random(rounds): - for i in range(rounds): - if i+1 in (1,rounds) or not (i+1) % 10: - msg('\rTesting random input data: {:4}/{} '.format(i+1,rounds)) - dlen = int(hexlify(os.urandom(4)),16) >> 18 - compare_hashes(dlen,os.urandom(dlen)) - msg('OK\n') - -msg(green('Testing MMGen implementation of Sha256()\n')) -test_K() -test_ref() -test_random(random_rounds) diff --git a/test/sha2test.py b/test/sha2test.py new file mode 100755 index 00000000..9a0a3b3e --- /dev/null +++ b/test/sha2test.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +test/sha2test.py: Test MMGen's SHA256 and SHA512 implementations +""" + +import sys,os +from binascii import hexlify +from mmgen.sha2 import Sha256,Sha512 +from mmgen.util import die + +assert len(sys.argv) in (2,3),"Test takes 1 or 2 arguments: test name, plus optional rounds count" +test = sys.argv[1].capitalize() +assert test in ('Sha256','Sha512'), "Valid choices for test are 'sha256' or 'sha512'" +random_rounds = int(sys.argv[2]) if len(sys.argv) == 3 else 500 + +def msg(s): sys.stderr.write(s) +def green(s): return '\033[32;1m' + s + '\033[0m' + +class TestSha2(object): + + def test_constants(self): + msg('Testing generated constants: ') + h = self.t_cls(b'foo') + if h.H_init != self.H_ref: + m = 'Generated constants H[] differ from reference value:\nReference:\n{}\nGenerated:\n{}' + die(3,m.format([hex(n) for n in self.H_ref],[hex(n) for n in h.H_init])) + if h.K != self.K_ref: + m = 'Generated constants K[] differ from reference value:\nReference:\n{}\nGenerated:\n{}' + die(3,m.format([hex(n) for n in self.K_ref],[hex(n) for n in h.K])) + msg('OK\n') + + def compare_hashes(self,dlen,data): + import hashlib + sha2 = getattr(hashlib,self.desc)(data).hexdigest().encode() + my_sha2 = self.t_cls(data).hexdigest() + if my_sha2 != sha2: + die(3,'Hashes do not match!\nmy_sha2: {}\nsha2: {}\n'.format(my_sha2,sha2)) + + def test_ref(self,input_n=None): + + inputs = ( + '','x','xa','the','7_chars''8charmsg' '9_charmsg','10_charmsg' + '8charmsg' * 8, + '8charmsg' * 7 + '7_chars', + '8charmsg' * 7 + '9_charmsg', + '\x00','\x00\x00','\x00'*256, + '\x0f','\x0f\x0f','\x0f'*256, + '\x0f\x0d','\x0e\x0e'*256, + '\x00'*511,'\x00'*512,'\x00'*513, + '\x0f'*512,'\x0f'*511,'\x0f'*513, + '\x00\x0f'*512,'\x0e\x0f'*511,'\x0a\x0d'*513, + '\x00\x0f'*1024,'\x0e\x0f'*1023,'\x0a\x0d'*1025 ) + + for i,data in enumerate([inputs[input_n]] if input_n is not None else inputs): + msg('\rTesting reference input data: {:4}/{} '.format(i+1,len(inputs))) + self.compare_hashes(len(data),data.encode()) + + msg('OK\n') + + def test_random(self,rounds): + for i in range(rounds): + if i+1 in (1,rounds) or not (i+1) % 10: + msg('\rTesting random input data: {:4}/{} '.format(i+1,rounds)) + dlen = int(hexlify(os.urandom(4)),16) >> 18 + self.compare_hashes(dlen,os.urandom(dlen)) + msg('OK\n') + +class TestSha512(TestSha2): + desc = 'sha512' + t_cls = Sha512 + H_ref = ( + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179 ) + K_ref = ( + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538, + 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe, + 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, + 0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab, + 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725, + 0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218, + 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, + 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c, + 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 ) + +class TestSha256(TestSha2): + desc = 'sha256' + t_cls = Sha256 + H_ref = ( + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 ) + K_ref = ( + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 ) + +msg(green('Testing MMGen implementation of {}\n'.format(test))) +t = globals()['Test'+test]() +t.test_constants() +t.test_ref() +t.test_random(random_rounds)