Browse Source

rewrite SHA2 implementation, support SHA512; update tests accordingly

- Note: the sha2 module is used by MMGen solely for generating Zcash
  addresses and viewkeys
MMGen 6 years ago
parent
commit
2b6dc95970
7 changed files with 354 additions and 232 deletions
  1. 1 1
      mmgen/addr.py
  2. 200 0
      mmgen/sha2.py
  3. 0 142
      mmgen/sha256.py
  4. 25 23
      scripts/test-release.sh
  5. 1 1
      setup.py
  6. 0 65
      test/sha256test.py
  7. 127 0
      test/sha2test.py

+ 1 - 1
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

+ 200 - 0
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 <mmgen@tuta.io>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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))

+ 0 - 142
mmgen/sha256.py

@@ -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 <mmgen@tuta.io>
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-"""
-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)

+ 25 - 23
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" ] && {

+ 1 - 1
setup.py

@@ -136,7 +136,7 @@ setup(
 			'mmgen.regtest',
 			'mmgen.rpc',
 			'mmgen.seed',
-			'mmgen.sha256',
+			'mmgen.sha2',
 			'mmgen.term',
 			'mmgen.tool',
 			'mmgen.tw',

+ 0 - 65
test/sha256test.py

@@ -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 <mmgen@tuta.io>
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-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)

+ 127 - 0
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 <mmgen@tuta.io>
+#
+# 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 <http://www.gnu.org/licenses/>.
+"""
+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)