import unittest import os import time import shutil import subprocess from binascii import hexlify, unhexlify from hashlib import sha1, sha256 from keys import SigningKey, VerifyingKey from keys import BadSignatureError import util from util import sigencode_der, sigencode_strings from util import sigdecode_der, sigdecode_strings from curves import Curve, UnknownCurveError from curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p import der class SubprocessError(Exception): pass def run_openssl(cmd): OPENSSL = "openssl" p = subprocess.Popen([OPENSSL] + cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, ignored = p.communicate() if p.returncode != 0: raise SubprocessError("cmd '%s %s' failed: rc=%s, stdout/err was %s" % (OPENSSL, cmd, p.returncode, stdout)) return stdout BENCH = False class ECDSA(unittest.TestCase): def test_basic(self): priv = SigningKey.generate() pub = priv.get_verifying_key() data = "blahblah" sig = priv.sign(data) self.failUnless(pub.verify(sig, data)) self.failUnlessRaises(BadSignatureError, pub.verify, sig, data+"bad") pub2 = VerifyingKey.from_string(pub.to_string()) self.failUnless(pub2.verify(sig, data)) def test_bad_usage(self): # sk=SigningKey() is wrong self.failUnlessRaises(TypeError, SigningKey) self.failUnlessRaises(TypeError, VerifyingKey) def test_lengths(self): default = NIST192p priv = SigningKey.generate() pub = priv.get_verifying_key() self.failUnlessEqual(len(pub.to_string()), default.verifying_key_length) sig = priv.sign("data") self.failUnlessEqual(len(sig), default.signature_length) if BENCH: print for curve in (NIST192p, NIST224p, NIST256p, NIST384p, NIST521p): start = time.time() priv = SigningKey.generate(curve=curve) pub1 = priv.get_verifying_key() keygen_time = time.time() - start pub2 = VerifyingKey.from_string(pub1.to_string(), curve) self.failUnlessEqual(pub1.to_string(), pub2.to_string()) self.failUnlessEqual(len(pub1.to_string()), curve.verifying_key_length) start = time.time() sig = priv.sign("data") sign_time = time.time() - start self.failUnlessEqual(len(sig), curve.signature_length) if BENCH: start = time.time() pub1.verify(sig, "data") verify_time = time.time() - start print "%s: siglen=%d, keygen=%0.3fs, sign=%0.3f, verify=%0.3f" \ % (curve.name, curve.signature_length, keygen_time, sign_time, verify_time) def test_serialize(self): seed = "secret" curve = NIST192p secexp1 = util.randrange_from_seed__trytryagain(seed, curve.order) secexp2 = util.randrange_from_seed__trytryagain(seed, curve.order) self.failUnlessEqual(secexp1, secexp2) priv1 = SigningKey.from_secret_exponent(secexp1, curve) priv2 = SigningKey.from_secret_exponent(secexp2, curve) self.failUnlessEqual(hexlify(priv1.to_string()), hexlify(priv2.to_string())) self.failUnlessEqual(priv1.to_pem(), priv2.to_pem()) pub1 = priv1.get_verifying_key() pub2 = priv2.get_verifying_key() data = "data" sig1 = priv1.sign(data) sig2 = priv2.sign(data) self.failUnless(pub1.verify(sig1, data)) self.failUnless(pub2.verify(sig1, data)) self.failUnless(pub1.verify(sig2, data)) self.failUnless(pub2.verify(sig2, data)) self.failUnlessEqual(hexlify(pub1.to_string()), hexlify(pub2.to_string())) def test_nonrandom(self): s = "all the entropy in the entire world, compressed into one line" def not_much_entropy(numbytes): return s[:numbytes] # we control the entropy source, these two keys should be identical: priv1 = SigningKey.generate(entropy=not_much_entropy) priv2 = SigningKey.generate(entropy=not_much_entropy) self.failUnlessEqual(hexlify(priv1.get_verifying_key().to_string()), hexlify(priv2.get_verifying_key().to_string())) # likewise, signatures should be identical. Obviously you'd never # want to do this with keys you care about, because the secrecy of # the private key depends upon using different random numbers for # each signature sig1 = priv1.sign("data", entropy=not_much_entropy) sig2 = priv2.sign("data", entropy=not_much_entropy) self.failUnlessEqual(hexlify(sig1), hexlify(sig2)) def failUnlessPrivkeysEqual(self, priv1, priv2): self.failUnlessEqual(priv1.privkey.secret_multiplier, priv2.privkey.secret_multiplier) self.failUnlessEqual(priv1.privkey.public_key.generator, priv2.privkey.public_key.generator) def failIfPrivkeysEqual(self, priv1, priv2): self.failIfEqual(priv1.privkey.secret_multiplier, priv2.privkey.secret_multiplier) def test_privkey_creation(self): s = "all the entropy in the entire world, compressed into one line" def not_much_entropy(numbytes): return s[:numbytes] priv1 = SigningKey.generate() self.failUnlessEqual(priv1.baselen, NIST192p.baselen) priv1 = SigningKey.generate(curve=NIST224p) self.failUnlessEqual(priv1.baselen, NIST224p.baselen) priv1 = SigningKey.generate(entropy=not_much_entropy) self.failUnlessEqual(priv1.baselen, NIST192p.baselen) priv2 = SigningKey.generate(entropy=not_much_entropy) self.failUnlessEqual(priv2.baselen, NIST192p.baselen) self.failUnlessPrivkeysEqual(priv1, priv2) priv1 = SigningKey.from_secret_exponent(secexp=3) self.failUnlessEqual(priv1.baselen, NIST192p.baselen) priv2 = SigningKey.from_secret_exponent(secexp=3) self.failUnlessPrivkeysEqual(priv1, priv2) priv1 = SigningKey.from_secret_exponent(secexp=4, curve=NIST224p) self.failUnlessEqual(priv1.baselen, NIST224p.baselen) def test_privkey_strings(self): priv1 = SigningKey.generate() s1 = priv1.to_string() self.failUnlessEqual(type(s1), str) self.failUnlessEqual(len(s1), NIST192p.baselen) priv2 = SigningKey.from_string(s1) self.failUnlessPrivkeysEqual(priv1, priv2) s1 = priv1.to_pem() self.failUnlessEqual(type(s1), str) self.failUnless(s1.startswith("-----BEGIN EC PRIVATE KEY-----")) self.failUnless(s1.strip().endswith("-----END EC PRIVATE KEY-----")) priv2 = SigningKey.from_pem(s1) self.failUnlessPrivkeysEqual(priv1, priv2) s1 = priv1.to_der() self.failUnlessEqual(type(s1), str) priv2 = SigningKey.from_der(s1) self.failUnlessPrivkeysEqual(priv1, priv2) priv1 = SigningKey.generate(curve=NIST256p) s1 = priv1.to_pem() self.failUnlessEqual(type(s1), str) self.failUnless(s1.startswith("-----BEGIN EC PRIVATE KEY-----")) self.failUnless(s1.strip().endswith("-----END EC PRIVATE KEY-----")) priv2 = SigningKey.from_pem(s1) self.failUnlessPrivkeysEqual(priv1, priv2) s1 = priv1.to_der() self.failUnlessEqual(type(s1), str) priv2 = SigningKey.from_der(s1) self.failUnlessPrivkeysEqual(priv1, priv2) def failUnlessPubkeysEqual(self, pub1, pub2): self.failUnlessEqual(pub1.pubkey.point, pub2.pubkey.point) self.failUnlessEqual(pub1.pubkey.generator, pub2.pubkey.generator) self.failUnlessEqual(pub1.curve, pub2.curve) def test_pubkey_strings(self): priv1 = SigningKey.generate() pub1 = priv1.get_verifying_key() s1 = pub1.to_string() self.failUnlessEqual(type(s1), str) self.failUnlessEqual(len(s1), NIST192p.verifying_key_length) pub2 = VerifyingKey.from_string(s1) self.failUnlessPubkeysEqual(pub1, pub2) priv1 = SigningKey.generate(curve=NIST256p) pub1 = priv1.get_verifying_key() s1 = pub1.to_string() self.failUnlessEqual(type(s1), str) self.failUnlessEqual(len(s1), NIST256p.verifying_key_length) pub2 = VerifyingKey.from_string(s1, curve=NIST256p) self.failUnlessPubkeysEqual(pub1, pub2) pub1_der = pub1.to_der() self.failUnlessEqual(type(pub1_der), str) pub2 = VerifyingKey.from_der(pub1_der) self.failUnlessPubkeysEqual(pub1, pub2) self.failUnlessRaises(der.UnexpectedDER, VerifyingKey.from_der, pub1_der+"junk") badpub = VerifyingKey.from_der(pub1_der) class FakeGenerator: def order(self): return 123456789 badcurve = Curve("unknown", None, FakeGenerator(), (1,2,3,4,5,6)) badpub.curve = badcurve badder = badpub.to_der() self.failUnlessRaises(UnknownCurveError, VerifyingKey.from_der, badder) pem = pub1.to_pem() self.failUnlessEqual(type(pem), str) self.failUnless(pem.startswith("-----BEGIN PUBLIC KEY-----"), pem) self.failUnless(pem.strip().endswith("-----END PUBLIC KEY-----"), pem) pub2 = VerifyingKey.from_pem(pem) self.failUnlessPubkeysEqual(pub1, pub2) def test_signature_strings(self): priv1 = SigningKey.generate() pub1 = priv1.get_verifying_key() data = "data" sig = priv1.sign(data) self.failUnlessEqual(type(sig), str) self.failUnlessEqual(len(sig), NIST192p.signature_length) self.failUnless(pub1.verify(sig, data)) sig = priv1.sign(data, sigencode=sigencode_strings) self.failUnlessEqual(type(sig), tuple) self.failUnlessEqual(len(sig), 2) self.failUnlessEqual(type(sig[0]), str) self.failUnlessEqual(type(sig[1]), str) self.failUnlessEqual(len(sig[0]), NIST192p.baselen) self.failUnlessEqual(len(sig[1]), NIST192p.baselen) self.failUnless(pub1.verify(sig, data, sigdecode=sigdecode_strings)) sig_der = priv1.sign(data, sigencode=sigencode_der) self.failUnlessEqual(type(sig_der), str) self.failUnless(pub1.verify(sig_der, data, sigdecode=sigdecode_der)) def test_hashfunc(self): sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) data = "security level is 128 bits" sig = sk.sign(data) vk = VerifyingKey.from_string(sk.get_verifying_key().to_string(), curve=NIST256p, hashfunc=sha256) self.failUnless(vk.verify(sig, data)) sk2 = SigningKey.generate(curve=NIST256p) sig2 = sk2.sign(data, hashfunc=sha256) vk2 = VerifyingKey.from_string(sk2.get_verifying_key().to_string(), curve=NIST256p, hashfunc=sha256) self.failUnless(vk2.verify(sig2, data)) vk3 = VerifyingKey.from_string(sk.get_verifying_key().to_string(), curve=NIST256p) self.failUnless(vk3.verify(sig, data, hashfunc=sha256)) class OpenSSL(unittest.TestCase): # test interoperability with OpenSSL tools. Note that openssl's ECDSA # sign/verify arguments changed between 0.9.8 and 1.0.0: the early # versions require "-ecdsa-with-SHA1", the later versions want just # "-SHA1" (or to leave out that argument entirely, which means the # signature will use some default digest algorithm, probably determined # by the key, probably always SHA1). # # openssl ecparam -name secp224r1 -genkey -out privkey.pem # openssl ec -in privkey.pem -text -noout # get the priv/pub keys # openssl dgst -ecdsa-with-SHA1 -sign privkey.pem -out data.sig data.txt # openssl asn1parse -in data.sig -inform DER # data.sig is 64 bytes, probably 56b plus ASN1 overhead # openssl dgst -ecdsa-with-SHA1 -prverify privkey.pem -signature data.sig data.txt ; echo $? # openssl ec -in privkey.pem -pubout -out pubkey.pem # openssl ec -in privkey.pem -pubout -outform DER -out pubkey.der def get_openssl_messagedigest_arg(self): v = run_openssl("version") # e.g. "OpenSSL 1.0.0 29 Mar 2010", or "OpenSSL 1.0.0a 1 Jun 2010", # or "OpenSSL 0.9.8o 01 Jun 2010" vs = v.split()[1].split(".") if vs >= ["1","0","0"]: return "-SHA1" else: return "-ecdsa-with-SHA1" # sk: 1:OpenSSL->python 2:python->OpenSSL # vk: 3:OpenSSL->python 4:python->OpenSSL # sig: 5:OpenSSL->python 6:python->OpenSSL def test_from_openssl_nist192p(self): return self.do_test_from_openssl(NIST192p, "prime192v1") def test_from_openssl_nist224p(self): return self.do_test_from_openssl(NIST224p, "secp224r1") def test_from_openssl_nist384p(self): return self.do_test_from_openssl(NIST384p, "secp384r1") def test_from_openssl_nist521p(self): return self.do_test_from_openssl(NIST521p, "secp521r1") def do_test_from_openssl(self, curve, curvename): # OpenSSL: create sk, vk, sign. # Python: read vk(3), checksig(5), read sk(1), sign, check mdarg = self.get_openssl_messagedigest_arg() if os.path.isdir("t"): shutil.rmtree("t") os.mkdir("t") run_openssl("ecparam -name %s -genkey -out t/privkey.pem" % curvename) run_openssl("ec -in t/privkey.pem -pubout -out t/pubkey.pem") data = "data" open("t/data.txt","wb").write(data) run_openssl("dgst %s -sign t/privkey.pem -out t/data.sig t/data.txt" % mdarg) run_openssl("dgst %s -verify t/pubkey.pem -signature t/data.sig t/data.txt" % mdarg) pubkey_pem = open("t/pubkey.pem").read() vk = VerifyingKey.from_pem(pubkey_pem) # 3 sig_der = open("t/data.sig","rb").read() self.failUnless(vk.verify(sig_der, data, # 5 hashfunc=sha1, sigdecode=sigdecode_der)) sk = SigningKey.from_pem(open("t/privkey.pem").read()) # 1 sig = sk.sign(data) self.failUnless(vk.verify(sig, data)) def test_to_openssl_nist192p(self): self.do_test_to_openssl(NIST192p, "prime192v1") def test_to_openssl_nist224p(self): self.do_test_to_openssl(NIST224p, "secp224r1") def test_to_openssl_nist384p(self): self.do_test_to_openssl(NIST384p, "secp384r1") def test_to_openssl_nist521p(self): self.do_test_to_openssl(NIST521p, "secp521r1") def do_test_to_openssl(self, curve, curvename): # Python: create sk, vk, sign. # OpenSSL: read vk(4), checksig(6), read sk(2), sign, check mdarg = self.get_openssl_messagedigest_arg() if os.path.isdir("t"): shutil.rmtree("t") os.mkdir("t") sk = SigningKey.generate(curve=curve) vk = sk.get_verifying_key() data = "data" open("t/pubkey.der","wb").write(vk.to_der()) # 4 open("t/pubkey.pem","wb").write(vk.to_pem()) # 4 sig_der = sk.sign(data, hashfunc=sha1, sigencode=sigencode_der) open("t/data.sig","wb").write(sig_der) # 6 open("t/data.txt","wb").write(data) open("t/baddata.txt","wb").write(data+"corrupt") self.failUnlessRaises(SubprocessError, run_openssl, "dgst %s -verify t/pubkey.der -keyform DER -signature t/data.sig t/baddata.txt" % mdarg) run_openssl("dgst %s -verify t/pubkey.der -keyform DER -signature t/data.sig t/data.txt" % mdarg) open("t/privkey.pem","wb").write(sk.to_pem()) # 2 run_openssl("dgst %s -sign t/privkey.pem -out t/data.sig2 t/data.txt" % mdarg) run_openssl("dgst %s -verify t/pubkey.pem -signature t/data.sig2 t/data.txt" % mdarg) class DER(unittest.TestCase): def test_oids(self): oid_ecPublicKey = der.encode_oid(1, 2, 840, 10045, 2, 1) self.failUnlessEqual(hexlify(oid_ecPublicKey), "06072a8648ce3d0201") self.failUnlessEqual(hexlify(NIST224p.encoded_oid), "06052b81040021") self.failUnlessEqual(hexlify(NIST256p.encoded_oid), "06082a8648ce3d030107") x = oid_ecPublicKey + "more" x1, rest = der.remove_object(x) self.failUnlessEqual(x1, (1, 2, 840, 10045, 2, 1)) self.failUnlessEqual(rest, "more") def test_integer(self): self.failUnlessEqual(der.encode_integer(0), "\x02\x01\x00") self.failUnlessEqual(der.encode_integer(1), "\x02\x01\x01") self.failUnlessEqual(der.encode_integer(127), "\x02\x01\x7f") self.failUnlessEqual(der.encode_integer(128), "\x02\x02\x00\x80") self.failUnlessEqual(der.encode_integer(256), "\x02\x02\x01\x00") #self.failUnlessEqual(der.encode_integer(-1), "\x02\x01\xff") def s(n): return der.remove_integer(der.encode_integer(n) + "junk") self.failUnlessEqual(s(0), (0, "junk")) self.failUnlessEqual(s(1), (1, "junk")) self.failUnlessEqual(s(127), (127, "junk")) self.failUnlessEqual(s(128), (128, "junk")) self.failUnlessEqual(s(256), (256, "junk")) self.failUnlessEqual(s(1234567890123456789012345678901234567890), ( 1234567890123456789012345678901234567890,"junk")) def test_number(self): self.failUnlessEqual(der.encode_number(0), "\x00") self.failUnlessEqual(der.encode_number(127), "\x7f") self.failUnlessEqual(der.encode_number(128), "\x81\x00") self.failUnlessEqual(der.encode_number(3*128+7), "\x83\x07") #self.failUnlessEqual(der.read_number("\x81\x9b"+"more"), (155, 2)) #self.failUnlessEqual(der.encode_number(155), "\x81\x9b") for n in (0, 1, 2, 127, 128, 3*128+7, 840, 10045): #, 155): x = der.encode_number(n) + "more" n1, llen = der.read_number(x) self.failUnlessEqual(n1, n) self.failUnlessEqual(x[llen:], "more") def test_length(self): self.failUnlessEqual(der.encode_length(0), "\x00") self.failUnlessEqual(der.encode_length(127), "\x7f") self.failUnlessEqual(der.encode_length(128), "\x81\x80") self.failUnlessEqual(der.encode_length(255), "\x81\xff") self.failUnlessEqual(der.encode_length(256), "\x82\x01\x00") self.failUnlessEqual(der.encode_length(3*256+7), "\x82\x03\x07") self.failUnlessEqual(der.read_length("\x81\x9b"+"more"), (155, 2)) self.failUnlessEqual(der.encode_length(155), "\x81\x9b") for n in (0, 1, 2, 127, 128, 255, 256, 3*256+7, 155): x = der.encode_length(n) + "more" n1, llen = der.read_length(x) self.failUnlessEqual(n1, n) self.failUnlessEqual(x[llen:], "more") def test_sequence(self): x = der.encode_sequence("ABC", "DEF") + "GHI" self.failUnlessEqual(x, "\x30\x06ABCDEFGHI") x1, rest = der.remove_sequence(x) self.failUnlessEqual(x1, "ABCDEF") self.failUnlessEqual(rest, "GHI") def test_constructed(self): x = der.encode_constructed(0, NIST224p.encoded_oid) self.failUnlessEqual(hexlify(x), "a007" + "06052b81040021") x = der.encode_constructed(1, unhexlify("0102030a0b0c")) self.failUnlessEqual(hexlify(x), "a106" + "0102030a0b0c") class Util(unittest.TestCase): def test_trytryagain(self): tta = util.randrange_from_seed__trytryagain for i in range(1000): seed = "seed-%d" % i for order in (2**8-2, 2**8-1, 2**8, 2**8+1, 2**8+2, 2**16-1, 2**16+1): n = tta(seed, order) self.failUnless(1 <= n < order, (1, n, order)) # this trytryagain *does* provide long-term stability self.failUnlessEqual("%x"%(tta("seed", NIST224p.order)), "6fa59d73bf0446ae8743cf748fc5ac11d5585a90356417e97155c3bc") def test_randrange(self): # util.randrange does not provide long-term stability: we might # change the algorithm in the future. for i in range(1000): entropy = util.PRNG("seed-%d" % i) for order in (2**8-2, 2**8-1, 2**8, 2**16-1, 2**16+1, ): # that oddball 2**16+1 takes half our runtime n = util.randrange(order, entropy=entropy) self.failUnless(1 <= n < order, (1, n, order)) def OFF_test_prove_uniformity(self): order = 2**8-2 counts = dict([(i, 0) for i in range(1, order)]) assert 0 not in counts assert order not in counts for i in range(1000000): seed = "seed-%d" % i n = util.randrange_from_seed__trytryagain(seed, order) counts[n] += 1 # this technique should use the full range self.failUnless(counts[order-1]) for i in range(1, order): print "%3d: %s" % (i, "*"*(counts[i]//100)) def __main__(): unittest.main() if __name__ == "__main__": __main__()