summaryrefslogtreecommitdiff
path: root/src/pycryptopp/test/test_ecdsa.py
blob: d70a0005a8a37821e6e475e9d0efb6987c44a7da (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env python

import random
import base64

import os
SEED = os.environ.get('REPEATABLE_RANDOMNESS_SEED', None)

if SEED is None:
    # Generate a seed which is fairly short (to ease cut-and-paste, writing it
    # down, etc.).  Note that Python's random module's seed() function is going
    # to take the hash() of this seed, which is a 32-bit value (currently) so
    # there is no point in making this seed larger than 32 bits.  Make it 30
    # bits, which conveniently fits into six base-32 chars.  Include a separator
    # because chunking facilitates memory (including working and short-term
    # memory) in humans.
    chars = "ybndrfg8ejkmcpqxot1uwisza345h769" # Zooko's choice, rationale in "DESIGN" doc in z-base-32 project
    SEED = ''.join([random.choice(chars) for x in range(3)] + ['-'] + [random.choice(chars) for x in range(3)])

import logging
logging.info("REPEATABLE_RANDOMNESS_SEED: %s\n" % SEED)
logging.info("In order to reproduce this run of the code, set the environment variable \"REPEATABLE_RANDOMNESS_SEED\" to %s before executing.\n" % SEED)
random.seed(SEED)

def seed_which_refuses(a):
    logging.warn("I refuse to reseed to %s -- I already seeded with %s.\n" % (a, SEED,))
    return
random.seed = seed_which_refuses

from random import randrange

import unittest

from pycryptopp.publickey import ecdsa

def randstr(n, rr=randrange):
    return ''.join([chr(rr(0, 256)) for x in xrange(n)])

from base64 import b32encode
def ab(x): # debuggery
    if len(x) >= 3:
        return "%s:%s" % (len(x), b32encode(x[-3:]),)
    elif len(x) == 2:
        return "%s:%s" % (len(x), b32encode(x[-2:]),)
    elif len(x) == 1:
        return "%s:%s" % (len(x), b32encode(x[-1:]),)
    elif len(x) == 0:
        return "%s:%s" % (len(x), "--empty--",)

def div_ceil(n, d):
    """
    The smallest integer k such that k*d >= n.
    """
    return (n/d) + (n%d != 0)

KEYBITS=192

# The number of bytes required for a seed to have the same security level as a
# key in this elliptic curve: 2 bits of public key per bit of security.
SEEDBITS=div_ceil(192, 2)
SEEDBYTES=div_ceil(SEEDBITS, 8)

# The number of bytes required to encode a public key in this elliptic curve.
PUBKEYBYTES=div_ceil(KEYBITS, 8)+1 # 1 byte for the sign of the y component

# The number of bytes requires to encode a signature in this elliptic curve.
SIGBITS=KEYBITS*2
SIGBYTES=div_ceil(SIGBITS, 8)

class Signer(unittest.TestCase):
    def test_construct(self):
        seed = randstr(SEEDBYTES)
        ecdsa.SigningKey(seed)

    def test_sign(self):
        seed = randstr(SEEDBYTES)
        signer = ecdsa.SigningKey(seed)
        sig = signer.sign("message")
        self.failUnlessEqual(len(sig), SIGBYTES)

    def test_sign_and_verify(self):
        seed = randstr(SEEDBYTES)
        signer = ecdsa.SigningKey(seed)
        sig = signer.sign("message")
        v = signer.get_verifying_key()
        self.failUnless(v.verify("message", sig))

    def test_sign_and_verify_emptymsg(self):
        seed = randstr(SEEDBYTES)
        signer = ecdsa.SigningKey(seed)
        sig = signer.sign("")
        v = signer.get_verifying_key()
        self.failUnless(v.verify("", sig))

    def test_construct_from_same_seed_is_reproducible(self):
        seed = randstr(SEEDBYTES)
        signer1 = ecdsa.SigningKey(seed)
        signer2 = ecdsa.SigningKey(seed)
        self.failUnlessEqual(signer1.get_verifying_key().serialize(), signer2.get_verifying_key().serialize())

        # ... and using different seeds constructs a different private key.
        seed3 = randstr(SEEDBYTES)
        assert seed3 != seed, "Internal error in Python random module's PRNG (or in pycryptopp's hacks to it to facilitate testing) -- got two identical strings from randstr(%s)" % SEEDBYTES
        signer3 = ecdsa.SigningKey(seed3)
        self.failIfEqual(signer1.get_verifying_key().serialize(), signer3.get_verifying_key().serialize())

        # Also try the all-zeroes string just because bugs sometimes are
        # data-dependent on zero or cause bogus zeroes.
        seed4 = '\x00'*SEEDBYTES
        assert seed4 != seed, "Internal error in Python random module's PRNG (or in pycryptopp's hacks to it to facilitate testing) -- got the all-zeroes string from randstr(%s)" % SEEDBYTES
        signer4 = ecdsa.SigningKey(seed4)
        self.failIfEqual(signer4.get_verifying_key().serialize(), signer1.get_verifying_key().serialize())

        signer5 = ecdsa.SigningKey(seed4)
        self.failUnlessEqual(signer5.get_verifying_key().serialize(), signer4.get_verifying_key().serialize())

    def test_construct_short_seed(self):
        try:
            ecdsa.SigningKey("\x00\x00\x00")
        except ecdsa.Error, le:
            self.failUnless("seed is required to be of length " in str(le), le)
        else:
           self.fail("Should have raised error from seed being too short.")

    def test_construct_bad_arg_type(self):
        try:
            ecdsa.SigningKey(1)
        except TypeError, le:
            self.failUnless("must be string" in str(le), le)
        else:
           self.fail("Should have raised error from seed being of the wrong type.")

class Verifier(unittest.TestCase):
    def test_from_signer_and_serialize_and_deserialize(self):
        seed = randstr(SEEDBYTES)
        signer = ecdsa.SigningKey(seed)

        verifier = signer.get_verifying_key()
        s1 = verifier.serialize()
        self.failUnlessEqual(len(s1), PUBKEYBYTES)
        ecdsa.VerifyingKey(s1)
        s2 = verifier.serialize()
        self.failUnlessEqual(s1, s2)

def flip_one_bit(s):
    assert s
    i = randrange(0, len(s))
    result = s[:i] + chr(ord(s[i])^(0x01<<randrange(0, 8))) + s[i+1:]
    assert result != s, "Internal error -- flip_one_bit() produced the same string as its input: %s == %s" % (result, s)
    return result

def randmsg():
    # Choose a random message size from a range probably large enough to
    # exercise any different code paths which depend on the message length.
    randmsglen = randrange(1, SIGBYTES*2+2)
    return randstr(randmsglen)

class SignAndVerify(unittest.TestCase):
    def _help_test_sign_and_check_good_keys(self, signer, verifier):
        msg = randmsg()

        sig = signer.sign(msg)
        self.failUnlessEqual(len(sig), SIGBYTES)
        self.failUnless(verifier.verify(msg, sig))

        # Now flip one bit of the signature and make sure that the signature doesn't check.
        badsig = flip_one_bit(sig)
        self.failIf(verifier.verify(msg, badsig))

        # Now generate a random signature and make sure that the signature doesn't check.
        badsig = randstr(len(sig))
        assert badsig != sig, "Internal error -- randstr() produced the same string twice: %s == %s" % (badsig, sig)
        self.failIf(verifier.verify(msg, badsig))

        # Now flip one bit of the message and make sure that the original signature doesn't check.
        badmsg = flip_one_bit(msg)
        self.failIf(verifier.verify(badmsg, sig))

        # Now generate a random message and make sure that the original signature doesn't check.
        badmsg = randstr(len(msg))
        assert badmsg != msg, "Internal error -- randstr() produced the same string twice: %s == %s" % (badmsg, msg)
        self.failIf(verifier.verify(badmsg, sig))

    def _help_test_sign_and_check_bad_keys(self, signer, verifier):
        """
        Make sure that this signer/verifier pair cannot produce and verify signatures.
        """
        msg = randmsg()

        sig = signer.sign(msg)
        self.failUnlessEqual(len(sig), SIGBYTES)
        self.failIf(verifier.verify(msg, sig))

    def test(self):
        seed = randstr(SEEDBYTES)
        signer = ecdsa.SigningKey(seed)
        verifier = signer.get_verifying_key()
        self._help_test_sign_and_check_good_keys(signer, verifier)

        vstr = verifier.serialize()
        self.failUnlessEqual(len(vstr), PUBKEYBYTES)
        verifier2 = ecdsa.VerifyingKey(vstr)
        self._help_test_sign_and_check_good_keys(signer, verifier2)

        signer2 = ecdsa.SigningKey(seed)
        self._help_test_sign_and_check_good_keys(signer2, verifier2)

        verifier3 = signer2.get_verifying_key()
        self._help_test_sign_and_check_good_keys(signer, verifier3)

        # Now test various ways that the keys could be corrupted or ill-matched.

        # Flip one bit of the public key.
        badvstr = flip_one_bit(vstr)
        try:
            badverifier = ecdsa.VerifyingKey(badvstr)
        except ecdsa.Error:
            # Ok, fine, the verifying key was corrupted and Crypto++ detected this fact.
            pass
        else:
            self._help_test_sign_and_check_bad_keys(signer, badverifier)

        # Randomize all bits of the public key.
        badvstr = randstr(len(vstr))
        assert badvstr != vstr, "Internal error -- randstr() produced the same string twice: %s == %s" % (badvstr, vstr)
        try:
            badverifier = ecdsa.VerifyingKey(badvstr)
        except ecdsa.Error:
            # Ok, fine, the key was corrupted and Crypto++ detected this fact.
            pass
        else:
            self._help_test_sign_and_check_bad_keys(signer, badverifier)

        # Flip one bit of the private key.
        badseed = flip_one_bit(seed)
        badsigner = ecdsa.SigningKey(badseed)
        self._help_test_sign_and_check_bad_keys(badsigner, verifier)

        # Randomize all bits of the private key.
        badseed = randstr(len(seed))
        assert badseed != seed, "Internal error -- randstr() produced the same string twice: %s == %s" % (badseed, seed)
        badsigner = ecdsa.SigningKey(badseed)
        self._help_test_sign_and_check_bad_keys(badsigner, verifier)

class Compatibility(unittest.TestCase):
    def test_compatibility(self):
        # Confirm that the KDF used by the SigningKey constructor doesn't
        # change without suitable backwards-compability
        seed = base64.b32decode('XS27TJRP3JBZKDEFBDKQ====')
        signer = ecdsa.SigningKey(seed)
        v1 = signer.get_verifying_key()
        vs = v1.serialize()
        vs32 = base64.b32encode(vs)
        self.failUnlessEqual(vs32, "ANPNDWJWHQXYSQMD4L36D7WQEGXA42MS5JRUFIWA")
        v2 = ecdsa.VerifyingKey(vs)
        #print base64.b32encode(signer.sign("message"))
        sig32 = "EA3Y7A4T62J3K6MUPJQN3WJ5S4SS53EGZXOSTQW7EQ7OXEMS6QJLYL63BLHMHZD7KFT37KEPJBAKI==="
        sig = base64.b32decode(sig32)
        self.failUnless(v1.verify("message", sig))
        self.failUnless(v2.verify("message", sig))

if __name__ == "__main__":
    unittest.main()