root / trunk / pyopencoin / oc / containers.py

Revision 241, 38.1 kB (checked in by ocmathew, 4 years ago)

Clear some fixmes. Performs some tests. Fix an error with HANDSHAKE

Line 
1import base64
2import json
3import types
4
5class Container(object):
6    r"""A generic container, handles serializing
7   
8    >>> c = Container(foo='foo',bar='bar')
9   
10    Accessing c.foo should not work, as the container has no
11    infos about its fields
12
13    >>> c.foo
14    Traceback (most recent call last):
15    ...     
16    AttributeError: 'Container' object has no attribute 'foo'
17   
18    Lets try again:
19    >>> Container.fields = ['foo','bar']
20    >>> c = Container(foo='foo',bar='bar')
21    >>> c
22    <Container(foo='foo',bar='bar')>
23
24    Serialize human readable - its actually json.
25    >>> c.content_part()
26    '[["foo","foo"],["bar","bar"]]'
27
28    Serialize to json
29    >>> j = c.toJson()
30    >>> j
31    '[["foo","foo"],["bar","bar"]]'
32
33    Lets deserialize
34    >>> c = Container()
35    >>> c
36    <Container(foo=None,bar=None)>
37   
38    >>> c.fromJson(j)
39    <Container(foo='foo',bar='bar')>
40   
41    >>> c
42    <Container(foo='foo',bar='bar')>
43
44    Add a codec for bar
45    >>> c.codecs['bar'] = {'encode':base64.encodestring,'decode':base64.decodestring}
46   
47    Lets look at the json now
48    >>> j2 = c.toJson()
49    >>> j2
50    '[["foo","foo"],["bar","YmFy\\n"]]'
51   
52    >>> c.fromJson(j2)
53    <Container(foo='foo',bar='bar')>
54    """
55
56    fields = []
57    codecs = {}
58    content_id = 'Container'
59   
60    def __init__(self,**kwargs):
61        """This would set up the data"""
62
63        for field in self.fields:
64            setattr(self,field,kwargs.get(field,None))
65        self.content_id = self.__class__.__name__     
66
67    def __repr__(self):
68        arguments = ','.join(["%s=%s" %(field,repr(getattr(self,field))) for field in self.fields])
69        return "<%s(%s)>" % (self.__class__.__name__,arguments)
70
71    def __str__(self):
72        return self.toJson()
73
74    def encodeField(self,fieldname):
75        '''returns the value of field in whatever string represnation'''
76
77        encoder = self.codecs.get(fieldname,{}).get('encode',lambda x: x)
78        return encoder(getattr(self,fieldname))
79
80    def decodeField(self,fieldname,text):
81        '''returns the value of field in whatever string represnation'''
82
83        decoder = self.codecs.get(fieldname,{}).get('decode',lambda x: x)
84        return decoder(text)       
85
86    def setCodec(self,fieldname,encoder=None,decoder=None):
87
88        donothing = lambda x: x
89        if not encoder:
90            encoder = donothing
91        if not decoder:
92            decoder = donothing
93       
94        self.codecs[fieldname] = {'encode':encoder,'decode':decoder}
95
96    def toPython(self, extranames=False):
97        return [(fieldname,self.encodeField(fieldname)) for fieldname in self.fields]
98
99    def fromPython(self,data):
100        i = 0
101        for fieldname in self.fields:
102            setattr(self,fieldname,self.decodeField(fieldname,data[i][1]))
103            i += 1
104        return self       
105
106    def content_part(self):
107        '''returns a human readable representation of the content'''
108
109        return self.toJson(False)
110
111        content = ';'.join(['"%s"="%s"' % t for t in self.toPython()])
112        return "%s={%s}" % (self.content_id,content)
113
114    def toJson(self, extranames=False):
115        return json.write(self.toPython(extranames))
116
117    def fromJson(self,text):
118        return self.fromPython(json.read(text))
119
120    def __eq__(self,other):
121        #return self.__dict__== other.__dict__
122        if hasattr(self, 'content_part') and hasattr(other, 'content_part'):
123            return self.content_part() == other.content_part()
124        else:
125            return False
126
127
128    def serialize(self):
129        import pickle
130        import base64
131
132        return base64.b64encode(pickle.dumps(self))
133
134
135def encodeTime(seconds):
136    #FIXME: this breaks if we are greater than whatever the epoc is (usually 2038)
137    import time
138    # we have to be careful here. Each field must be atleast 2 characters long.
139    instant = ['%02d' % k for k in time.gmtime(seconds)[:6]]
140    return '-'.join(instant[:3]) + 'T' + ':'.join(instant[3:6]) + 'Z'
141
142def decodeTime(s):
143    """decodes a time
144   
145    >>> decodeTime('2008-02-01T00:00:00Z')
146    1201824000
147
148    >>> decodeTime('foo')
149    Traceback (most recent call last):
150    TypeError: ...
151
152    """
153    #FIXME: this breaks if we are greater than whatever the epoc is (usually 2038)
154    import time, calendar
155    try:
156        date, t = s.split('T')
157        year, month, day = date.split('-')
158        if len(year) < 4 or len(month) != 2 or len(day) != 2:
159            raise TypeError('Encoding of date "%s" is incorrect' % s)
160        if t[-1] != 'Z':
161            raise TypeError('Encoding of date "%s" is incorrect' % s)
162        t = t[:-1] # get rid of the 'Z'
163        hour, min, sec = t.split(':')
164        # Okay. If all these splits worked, we know that we have correctly formatted string.
165        # so let strptime deal with anything odd
166    except ValueError:
167        raise TypeError('Encoding of date "%s" is incorrect' % s)
168
169    struct = time.strptime(s, '%Y-%m-%dT%H:%M:%SZ')
170    return calendar.timegm(struct)
171
172def validateIntString(s):
173    """Returns a string if s is a an encoding of the integer value of s with no spaces or leading/trailing zeros or decimal points."""
174    if str(int(s)) == s:
175        return s
176    else:
177        raise TypeError('Encoding of int "%s" is incorrect' % s)
178
179def validateIntStringList(l):
180    """Returns a list of strings if each element in the list passes validateIntString."""
181    for s in l:
182        validateIntString(s)
183
184    return l
185
186def validateOptionsList(l):
187    """Validates an options list is a list of (key, val) pairs of strings."""
188    import types
189    if not isinstance(l, types.ListType):
190        raise TypeError('Not a valid options list')
191    d = {}
192    try:
193        for key, val in l:
194            if key in d:
195                raise TypeError('Not a valid options list')
196            if not isinstance(key, types.StringType):
197                raise TypeError('Not a valid options list')
198            if not isinstance(val, types.StringType):
199                raise TypeError('Not a valid options list')
200            d[key] = val
201    except ValueError:
202        raise TypeError('Not a valid options list')
203
204    if not 'version' in d:
205        raise TypeError('Not a valid options list')
206
207    return l
208       
209
210class Signature(Container):
211    """The signature container (a combination of the keyprint and signature fields.
212   
213    >>> s = Signature(keyprint='0',signature='*')
214    >>> s.toJson()
215    '[["keyprint","MA=="],["signature","Kg=="]]'
216    >>> s == Signature().fromJson(s.toJson())
217    True
218    """
219    fields = ['keyprint',
220              'signature']
221
222    codecs = {'keyprint':{'encode':base64.b64encode, 'decode':base64.b64decode},
223              'signature':{'encode':base64.b64encode, 'decode':base64.b64decode}}
224   
225
226class ContainerWithSignature(Container):
227    """A container with an optional signature field.
228
229    To activate the Json-ing of the signature, supply an argument which is true to
230    the function.
231
232    >>> class TestContainer(ContainerWithSignature):
233    ...     fields = ['string', 'number']
234    ...     codecs = {'number':{'encode':base64.b64encode, 'decode':base64.b64decode}}
235
236    >>> signature = Signature(keyprint='0', signature='*')
237
238    >>> test1 = TestContainer(string='hello', number='@')
239    >>> test1_j = test1.content_part()
240    >>> test1_j
241    '[["string","hello"],["number","QA=="]]'
242
243    >>> test1.toPython(False)
244    [('string', 'hello'), ('number', 'QA==')]
245
246    >>> test2 = TestContainer().fromPython(test1.toPython(False))
247    >>> test2 == test1
248    True
249
250    >>> test3 = TestContainer().fromJson(test1_j)
251    >>> test3 == test1
252    True
253
254    Check to make sure toJson fails if we force it to use the signature and we don't
255    have it
256    >>> test4_j = test1.toJson()
257    Traceback (most recent call last):
258    ...
259    AttributeError: 'NoneType' object has no attribute 'toPython'
260
261    >>> test1.content_part() == test1_j
262    True
263
264    >>> test5 = TestContainer(string='hello', number='@', signature=signature)
265    >>> test5_j = test5.toJson()
266    >>> test5_j
267    '[["string","hello"],["number","QA=="],["signature",[["keyprint","MA=="],["signature","Kg=="]]]]'
268
269    >>> test5.toPython()
270    [('string', 'hello'), ('number', 'QA=='), ['signature', [('keyprint', 'MA=='), ('signature', 'Kg==')]]]
271    >>> TestContainer().fromPython(test5.toPython()).toPython()
272    [('string', 'hello'), ('number', 'QA=='), ['signature', [('keyprint', 'MA=='), ('signature', 'Kg==')]]]
273
274    >>> test6 = TestContainer().fromJson(test1.content_part())
275    >>> test6.signature = signature
276    >>> test6 == test5
277    True
278
279    >>> test5.content_part() == test1_j
280    True
281   
282    TODO: Add verify_signature checking
283    """
284    def __init__(self, **kwargs):
285        Container.__init__(self,**kwargs)
286        self.jsontext = None
287        self.signature = kwargs.get('signature')
288
289
290    def toPython(self, extranames=True):
291        if not extranames:
292            return [(fieldname,self.encodeField(fieldname)) for fieldname in self.fields]
293        else:
294            fields = [(fieldname, self.encodeField(fieldname)) for fieldname in self.fields]
295            fields.append(['signature', self.signature.toPython()])
296            return fields
297
298    def fromPython(self, data):
299        i = 0
300        for fieldname in self.fields:
301            setattr(self,fieldname,self.decodeField(fieldname,data[i][1]))
302            i += 1
303       
304        if len(data) - 1 == len(self.fields) and data[i][0] == 'signature':
305            s = Signature()
306            self.signature = s.fromPython(data[i][1])
307           
308        return self
309
310    def toJson(self, extranames=True):
311        return json.write(self.toPython(extranames))
312
313    def fromJson(self,text):
314        return self.fromPython(json.read(text))
315
316    def verifySignature(self, signature_algorithm, hashing_algorithm, key):
317
318        content_part = self.content_part()
319        hasher = hashing_algorithm(content_part)
320        signer = signature_algorithm(key)
321       
322        if hashing_algorithm(str(key)).digest() != self.signature.keyprint:
323            return False
324       
325        return signer.verify(hasher.digest(), self.signature.signature)
326
327############################ CDD ####################################
328
329class CurrencyDescriptionDocument(ContainerWithSignature):
330    """The CurrencyDescriptionDocument container
331
332    Lets test a bit
333    >>> import crypto
334    >>> ics = crypto.CryptoContainer(signing=crypto.RSASigningAlgorithm,
335    ...                              blinding=crypto.RSABlindingAlgorithm,
336    ...                              hashing=crypto.SHA256HashingAlgorithm)
337   
338    >>> cdd = CDD(standard_identifier = 'http://opencoin.org/OpenCoinProtocol/1.0',
339    ...           currency_identifier = 'http://opencent.net/OpenCent',
340    ...           short_currency_identifier = 'OC',
341    ...           issuer_service_location = 'opencoin://issuer.opencent.net:8002',
342    ...           denominations = ['1', '2', '5', '10', '20', '50', '100', '200', '500', '1000'],
343    ...           issuer_cipher_suite = ics,
344    ...           options = [['version', '0']],
345    ...           issuer_public_master_key = crypto.RSAKeyPair(e=17L,n=3233L))
346
347    >>> j = cdd.content_part()
348    >>> j
349    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["short_currency_identifier","OC"],["issuer_service_location","opencoin://issuer.opencent.net:8002"],["denominations",["1","2","5","10","20","50","100","200","500","1000"]],["issuer_cipher_suite",["RSASigningAlgorithm","RSABlindingAlgorithm","SHA256HashingAlgorithm"]],["options",[["version","0"]]],["issuer_public_master_key","DKE=,EQ=="]]'
350 
351    >>> cdd2 = CDD().fromJson(j)
352    >>> cdd2 == cdd
353    True
354
355    >>> sig = Signature(keyprint=']', signature='V')
356    >>> cdd2.signature = sig
357
358    >>> cdd2.toJson()
359    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["short_currency_identifier","OC"],["issuer_service_location","opencoin://issuer.opencent.net:8002"],["denominations",["1","2","5","10","20","50","100","200","500","1000"]],["issuer_cipher_suite",["RSASigningAlgorithm","RSABlindingAlgorithm","SHA256HashingAlgorithm"]],["options",[["version","0"]]],["issuer_public_master_key","DKE=,EQ=="],["signature",[["keyprint","XQ=="],["signature","Vg=="]]]]'
360   
361   
362    >>> from tests import CDD as test_cdd
363
364    >>> test_j = test_cdd.toJson()
365
366    >>> test_cdd2 = CDD().fromJson(test_j)
367    >>> test_cdd2 == test_cdd
368    True
369
370    >>> test_cdd2.signature == test_cdd.signature
371    True
372
373    >>> test_cdd3 = CDD().fromPython(test_cdd.toPython())
374    >>> test_cdd3 == test_cdd
375    True
376
377    >>> test_cdd.verify_self()
378    True
379    """
380
381    from crypto import encodeCryptoContainer, decodeCryptoContainer, decodeRSAKeyPair
382   
383    fields = ['standard_identifier', 
384              'currency_identifier', 
385              'short_currency_identifier', 
386              'issuer_service_location', 
387              'denominations', 
388              'issuer_cipher_suite', 
389              'options',
390              'issuer_public_master_key']
391
392    codecs = {'issuer_cipher_suite':{'encode':encodeCryptoContainer,'decode':decodeCryptoContainer},
393              'issuer_public_master_key':{'encode':str, 'decode':decodeRSAKeyPair},
394              'denominations':{'encode':list, 'decode':validateIntStringList},
395              'options':{'encode':list, 'decode':validateOptionsList}}
396
397
398    def __init__(self,**kwargs):
399        ContainerWithSignature.__init__(self, **kwargs)
400        self.keytype = kwargs.get('keytype', None)
401
402    def verify_self(self):
403        """Verifies the self-signed certificate."""
404        import crypto       
405
406        if 'version' not in dict(self.options):
407            return False # required option
408        ics = self.issuer_cipher_suite
409        return self.verifySignature(ics.signing,
410                                    ics.hashing,
411                                    self.issuer_public_master_key)
412
413
414CDD = CurrencyDescriptionDocument
415
416
417class MintKey(ContainerWithSignature):
418    """The MintKey container.
419
420    The MintKey container holds everything (almost?) everything required to verify a
421    Token.
422
423    TODO: Some test go here. Things to test
424
425    >>> from calendar import timegm
426    >>> from tests import CDD, CDD_private, addSignature
427    >>> import crypto, copy
428    >>> private, public = crypto.createRSAKeyPair(512)
429    >>> key_id = public.key_id(CDD.issuer_cipher_suite.hashing)
430
431    >>> mintKey = MintKey(key_identifier=key_id,
432    ...                   currency_identifier='http://opencent.net/OpenCent',
433    ...                   denomination="1",
434    ...                   not_before=timegm((2008,1,1,0,0,0)),
435    ...                   key_not_after=timegm((2008,2,1,0,0,0)),
436    ...                   token_not_after=timegm((2008,4,1,0,0,0)),
437    ...                   public_key=public)
438                         
439    >>> hash_alg = CDD.issuer_cipher_suite.hashing
440    >>> sign_alg = CDD.issuer_cipher_suite.signing
441   
442    >>> def addSignatureAndVerify(mintKey, CDD, signing_key):
443    ...     ics = CDD.issuer_cipher_suite
444    ...     mintKey = addSignature(mintKey, ics.hashing, ics.signing,
445    ...                     signing_key, mintKey.key_identifier)
446    ...     return mintKey.verify_with_CDD(CDD)
447
448    >>> mintKey = addSignature(mintKey, hash_alg, sign_alg, CDD_private, CDD.signature.keyprint)
449
450    >>> mintKey.verify_with_CDD(CDD)
451    True
452
453    >>> mintKey.toJson()
454    '[["key_identifier","..."],["currency_identifier","http://opencent.net/OpenCent"],["denomination","1"],["not_before","2008-01-01T00:00:00Z"],["key_not_after","2008-02-01T00:00:00Z"],["token_not_after","2008-04-01T00:00:00Z"],["public_key","..."],["signature",[["keyprint","hxz5pRwS+RFp88qQliXYm3R5uNighktwxqEh4RMOuuk="],["signature","..."]]]]'
455
456    Well, we are going to test that verify_with_CDD works now. We've already
457    built helper a helper function, addSignatureAndVerify. The reason
458    we have it is because if we modify the fields, the signature checking will
459    fail. So we have to have an incorrect field and a correct signature, to make
460    sure we are testing only one error condition at a time.
461    >>> mintKey2 = copy.deepcopy(mintKey)
462    >>> mintKey2.signature.signature = "foo"
463    >>> mintKey2.verify_with_CDD(CDD)
464    False
465
466    >>> mintKey3 = copy.deepcopy(mintKey)
467    >>> mintKey3.signature.keyprint = "foo"
468    >>> mintKey3.verify_with_CDD(CDD)
469    False
470
471    >>> mintKey4 = copy.deepcopy(mintKey)
472    >>> mintKey4.key_identifier = "foo"
473    >>> addSignatureAndVerify(mintKey2, CDD, CDD_private)
474    False
475
476    >>> mintKey5 = copy.deepcopy(mintKey)
477    >>> mintKey5.public_key = "foo"
478    >>> addSignatureAndVerify(mintKey2, CDD, CDD_private)
479    False
480
481    >>> mintKey6 = copy.deepcopy(mintKey)
482    >>> mintKey6.currency_identifier = "foo"
483    >>> addSignatureAndVerify(mintKey6, CDD, CDD_private)
484    False
485
486    >>> mintKey7 = copy.deepcopy(mintKey)
487    >>> mintKey7.denomination = "1.4"
488    >>> addSignatureAndVerify(mintKey7, CDD, CDD_private)
489    False
490
491    >>> mintKey8 = copy.deepcopy(mintKey)
492    >>> mintKey8.signature = None
493    >>> mintKey8.verify_with_CDD(CDD)
494    False
495
496    Just to make sure we didn't mess up something on the way...
497    >>> mintKey.verify_with_CDD(CDD)
498    True
499
500    >>> mintKey == MintKey().fromPython(mintKey.toPython())
501    True
502
503    Okay. We'll test verify_time now. It returns (can_mint, can_redeem)
504    The tuple input to timegm is (year, month, day, hour, minute, second).
505    The h/m/s is the same as on a normal clock. h/m/s is in 24 hour time
506    with the day starting at 0:0:0 and the end of the day 24:0:0. I think
507    that day1 24:0:0 is the same as day2 0:0:0.
508    >>> mintKey.verify_time(timegm((2007,12,1,0,0,0)))
509    (False, False)
510
511    >>> mintKey.verify_time(timegm((2008,1,1,0,0,0)))
512    (True, True)
513
514    >>> mintKey.verify_time(timegm((2008,2,1,0,0,0)))
515    (True, True)
516
517    >>> mintKey.verify_time(timegm((2008,2,1,0,0,1)))
518    (False, True)
519
520    >>> mintKey.verify_time(timegm((2008,4,1,0,0,0)))
521    (False, True)
522
523    >>> mintKey.verify_time(timegm((2008,4,1,0,0,1)))
524    (False, False)
525    """
526
527    fields = ['key_identifier', 
528              'currency_identifier', 
529              'denomination', 
530              'not_before', 
531              'key_not_after', 
532              'token_not_after',
533              'public_key']
534
535    from crypto import decodeRSAKeyPair
536
537    codecs = {'key_identifier':{'encode':base64.b64encode,'decode':base64.b64decode},
538              'public_key':{'encode':str,'decode':decodeRSAKeyPair},
539              'not_before':{'encode':encodeTime,'decode':decodeTime},
540              'key_not_after':{'encode':encodeTime,'decode':decodeTime},
541              'token_not_after':{'encode':encodeTime,'decode':decodeTime},
542              'denomination':{'encode':str,'decode':validateIntString}}
543
544    def __init__(self, **kwargs):
545        ContainerWithSignature.__init__(self, **kwargs)
546        self.keytype = kwargs.get('keytype', None)
547        if self.denomination and not isinstance(self.denomination, types.StringType):
548            raise Exception('Tried to set a denomination that was not a string')
549
550    def verify_with_CDD(self, currency_description_document):
551        """verify_with_CDD verifies the mint key against the CDD ensuring valid values
552        matching the CDD and the signature validity.i
553           
554        It assumes the CDD is valid. If not, some tests here willl pass (notably the
555        signature keyprint check)
556        """
557
558        cdd = currency_description_document
559
560        if not self.signature:
561            return False # if we have no signature, we are not valid (or verifiable)
562
563        if self.signature.keyprint != cdd.signature.keyprint:
564            return False # if they aren't the same master key, it isn't valid
565
566        if self.denomination not in cdd.denominations:
567            return False # if we are not a denomination, we aren't valid
568
569        if self.currency_identifier != cdd.currency_identifier:
570            return False # we have to be using the same currency identifier
571
572        if self.key_identifier != cdd.issuer_cipher_suite.hashing(str(self.public_key)).digest():
573            return False # the key identifier is not valid
574
575        signing, hashing = cdd.issuer_cipher_suite.signing, cdd.issuer_cipher_suite.hashing
576        return self.verifySignature(signing, hashing, cdd.issuer_public_master_key)
577       
578    def verify_time(self, time):
579        """Whether the container is valid at time. Returns a tuple of (can_mint, can_redeem)."""
580
581        can_mint = time >= self.not_before and time <= self.key_not_after
582        can_redeem = time >= self.not_before and time <= self.token_not_after
583
584        return (can_mint, can_redeem)
585
586
587class CurrencyBase(Container):
588    """The base class for the currency types.
589   
590    Test the adding of currencies
591    >>> b = CurrencyBase(standard_identifier = 'http://opencoin.org/OpenCoinProtocol/1.0',
592    ...                 currency_identifier = 'http://opencent.net/OpenCent',
593    ...                 denomination = '1',
594    ...                 key_identifier = 'keyid',
595    ...                 serial = '1')
596
597    #>>> b.value
598    #1
599    >>> import copy
600    >>> c = copy.copy(b)
601    >>> b + c
602    2
603    >>> sum([b,c])
604    2
605
606    Test that going from a python copy works
607    >>> d = CurrencyBase().fromPython(b.toPython())
608    >>> b + c + d
609    3
610
611    Test proper encoding/decoding of key_identifier and serial
612    >>> b = CurrencyBase(standard_identifier = 'http://opencoin.org/OpenCoinProtocol/1.0',
613    ...                 currency_identifier = 'http://opencent.net/OpenCent',
614    ...                 denomination = '1',
615    ...                 key_identifier = 'a',
616    ...                 serial = 'b')
617
618    >>> j = b.toJson()
619    >>> j
620    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["denomination","1"],["key_identifier","YQ=="],["serial","Yg=="]]'
621
622    >>> b == CurrencyBase().fromJson(j)
623    True
624   
625    """
626   
627    fields = ['standard_identifier', 
628              'currency_identifier', 
629              'denomination', 
630              'key_identifier', 
631              'serial']
632
633    # signature codec is in place for CurrencyCoin
634    codecs = {'key_identifier':{'encode':base64.b64encode,'decode':base64.b64decode},
635              'serial':{'encode':base64.b64encode,'decode':base64.b64decode},
636              'denomination':{'encode':str,'decode':validateIntString}}
637
638
639    def __init__(self, **kwargs):
640        Container.__init__(self, **kwargs)
641
642    def __add__(self,other):
643        val = self.getValue()
644        if type(other) == type(self) and self.sameCurrency(other):
645            return val + other.getValue()
646        elif type(other) == int:
647            return val + other
648        else:
649            raise NotImplementedError
650           
651
652    __radd__ = __add__ 
653
654    def getValue(self):
655        try:
656            return int(self.denomination)
657        except Exception:
658            pass 
659
660    def sameCurrency(self, other):
661        """Verifies the two objects are the same currency."""
662        if self.standard_identifier != other.standard_identifier:
663            return False
664
665        if self.currency_identifier != other.currency_identifier:
666            return False
667
668        return True
669
670
671    def validate_with_CDD_and_MintKey(self, currency_description_document, mint_key):
672        """Validates the currency with the cdd and mint key. Also verifies mint_key (for my safety)."""
673
674        cdd = currency_description_document
675
676        if not mint_key.verify_with_CDD(cdd):
677            return False
678
679        if self.standard_identifier != cdd.standard_identifier:
680            return False
681
682        if self.currency_identifier != mint_key.currency_identifier:
683            return False
684
685        if self.denomination != mint_key.denomination:
686            return False
687
688        if self.key_identifier != mint_key.key_identifier:
689            return False
690
691        return True # Everything checks out
692
693   
694
695class CurrencyBlank(CurrencyBase):
696    """CurrencyBlank is a blank without signature. It can blind and unblind.
697
698    Pieces of this container are messy. It fills its own fields (serial), it
699    holds additional information (blind_factor), and it passes that information
700    on to the blind algorithm it stores.
701
702    Everything in the case of a quick turnaround is easy. You create a blank,
703    generate a serial, blind the blank, [receive the signed blind], unblind the
704    signature, and make a new coin.
705   
706    Allowing testing however means that we need to be able to give a blind_factor.
707    We also need to be able to stop the transaction partway after sending the
708    signed blank, reset the software, and continue the transaction. With this in
709    mind,
710
711    >>> blank = CurrencyBlank(standard_identifier='http://OpenCoin/1.0/',
712    ...                       currency_identifier='http://OpenCent',
713    ...                       denomination='1',
714    ...                       key_identifier='cent')
715
716    >>> blank.toJson()
717    Traceback (most recent call last):
718    TypeError: b2a_base64...
719
720    >>> blank.serial = '123'
721
722    >>> blank.toJson()
723    '[["standard_identifier","http://OpenCoin/1.0/"],["currency_identifier","http://OpenCent"],["denomination","1"],["key_identifier","Y2VudA=="],["serial","MTIz"]]'
724
725    >>> blank.serial = None
726    >>> blank.generateSerial()
727    >>> base64.b64encode(blank.serial)
728    '...'
729
730    >>> blank.generateSerial()
731    Traceback (most recent call last):
732    Exception: Cannot generate a new serial when the serial is set.
733   
734   
735    >>> from calendar import timegm
736    >>> from tests import CDD, CDD_private, keys512, addSignature
737    >>> import crypto, copy
738
739    >>> def addSignatureAndVerify(mintKey, CDD, signing_key):
740    ...     ics = CDD.issuer_cipher_suite
741    ...     mintKey = addSignature(mintKey, ics.hashing, ics.signing,
742    ...                     signing_key, mintKey.key_identifier)
743    ...     return mintKey.verify_with_CDD(CDD)
744   
745    >>> private1 = keys512[0]
746    >>> public1 = private1.newPublicKeyPair()
747                         
748    >>> hash_alg = CDD.issuer_cipher_suite.hashing
749    >>> sign_alg = CDD.issuer_cipher_suite.signing
750    >>> blind_alg = CDD.issuer_cipher_suite.blinding
751   
752    >>> mintKey1 = MintKey(key_identifier=public1.key_id(hash_alg),
753    ...                   currency_identifier='http://opencent.net/OpenCent',
754    ...                   denomination='1',
755    ...                   not_before=timegm((2008,1,1,0,0,0)),
756    ...                   key_not_after=timegm((2008,2,1,0,0,0)),
757    ...                   token_not_after=timegm((2008,4,1,0,0,0)),
758    ...                   public_key=public1)
759    >>> mintKey1 = addSignature(mintKey1, hash_alg, sign_alg, CDD_private, CDD.signature.keyprint)
760
761    >>> mintKey1.verify_with_CDD(CDD)
762    True
763
764    >>> private5 = keys512[1]
765    >>> public5 = private5.newPublicKeyPair()
766    >>> mintKey5 = MintKey(key_identifier=public5.key_id(hash_alg),
767    ...                   currency_identifier='http://opencent.net/OpenCent',
768    ...                   denomination='5',
769    ...                   not_before=timegm((2008,1,1,0,0,0)),
770    ...                   key_not_after=timegm((2008,2,1,0,0,0)),
771    ...                   token_not_after=timegm((2008,4,1,0,0,0)),
772    ...                   public_key=public5)
773    >>> mintKey5 = addSignature(mintKey5, hash_alg, sign_alg, CDD_private, CDD.signature.keyprint)
774
775    >>> mintKey5.verify_with_CDD(CDD)
776    True
777   
778    >>> blank = CurrencyBlank(standard_identifier=CDD.standard_identifier,
779    ...                       currency_identifier=CDD.currency_identifier,
780    ...                       denomination='1',
781    ...                       key_identifier=mintKey1.key_identifier,
782    ...                       serial='abcdefghijklmnopqrstuvwxyz')
783
784    >>> blank.toJson()
785    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["denomination","1"],["key_identifier","sj17RxE1hfO06+oTgBs9Z7xLut/3NN+nHJbXSJYTks0="],["serial","YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="]]'
786
787    >>> blank.content_part() == blank.toJson()
788    True
789
790    >>> blind_value = blank.blind_blank(CDD, mintKey1, blind_factor='blind factor')
791    >>> base64.b64encode(blind_value)
792    'FtDSI1eT2FsXK+R/zJVk9mGTRan4KQsogVmSMFts3kVrdV4y3mkIuYEaZ3B0ZP491rqR0QtIlVEAYf8sQNgbhQ=='
793   
794    Do some magic as the mint
795    >>> blind_sig = sign_alg(private1).sign(blind_value)
796    >>> base64.b64encode(blind_sig)
797    'ZEDz4RHVwUByR+QXwgZcNeIyg9T3hAgxl0taabVKjbv0DbxTmHYK9fgqlCtBSAvmNntk03DMKrIPpaLuAV3UcA=='
798
799    >>> clear_sig = blank.unblind_signature(blind_sig)
800    >>> base64.b64encode(clear_sig)
801    'HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdpYhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A=='
802
803    Check for the same signature
804    >>> clear_sig == sign_alg(private1).sign(hash_alg(blank.content_part()).digest())
805    True
806
807    >>> coin = blank.newCoin(clear_sig) # perform seperate testing
808    >>> coin.validate_with_CDD_and_MintKey(CDD, mintKey1)
809    True
810
811    >>> coin.toJson()
812    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["denomination","1"],["key_identifier","sj17RxE1hfO06+oTgBs9Z7xLut/3NN+nHJbXSJYTks0="],["serial","YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="],["signature","HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdpYhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A=="]]'
813
814    >>> coin2 = blank.newCoin(clear_sig, CDD, mintKey1)
815   
816    Test newCoin failures... newCoin performs a check if given a CDD
817    or a MintKey. if it does not validate, it will raise an Exception.
818    If we only give a MintKey or a CDD, it attempts to perform the
819    validation, although it will fail. This is to prevent any accidents
820    in arguments where the coin will not be checked when we want it to.
821    >>> coin3 = blank.newCoin(clear_sig, CDD, mintKey5)
822    Traceback (most recent call last):
823    BlankError: New coin does not validate!
824
825    >>> coin4 = blank.newCoin(clear_sig, CDD)
826    Traceback (most recent call last):
827    AttributeError: ...
828
829    >>> coin5 = blank.newCoin(clear_sig, mint_key=mintKey1)
830    Traceback (most recent call last):
831    AttributeError: ...
832
833    Test other blank functions.
834    >>> blank2 = CurrencyBlank(standard_identifier=CDD.standard_identifier,
835    ...                       currency_identifier=CDD.currency_identifier,
836    ...                       denomination='1',
837    ...                       key_identifier=mintKey1.key_identifier,
838    ...                       serial='abcdefghijklmnopqrstuvwxyz')
839    >>> blank2.setBlind(blind_alg, mintKey1.public_key, 'blind factor')
840    >>> base64.b64encode(blank2.unblind_signature(blind_sig))
841    'HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdpYhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A=='
842
843    >>> blank3 = CurrencyBlank(standard_identifier=CDD.standard_identifier,
844    ...                       currency_identifier=CDD.currency_identifier,
845    ...                       denomination='1',
846    ...                       key_identifier=mintKey1.key_identifier,
847    ...                       serial='abcdefghijklmnopqrstuvwxyz',
848    ...                       blind_factor='blind factor')
849    >>> blank3.setBlind(blind_alg, mintKey1.public_key)
850    >>> base64.b64encode(blank3.unblind_signature(blind_sig))
851    'HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdpYhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A=='
852
853    And this is the normal and standard way to make a blank and blind
854    >>> blank4 = CurrencyBlank(standard_identifier=CDD.standard_identifier,
855    ...                       currency_identifier=CDD.currency_identifier,
856    ...                       denomination='1',
857    ...                       key_identifier=mintKey1.key_identifier)
858    >>> blank4.generateSerial()
859    >>> blind_value4 = blank4.blind_blank(CDD, mintKey1)
860    >>> base64.b64encode(blind_value4)
861    '...'
862
863    The blind_factor can be used to recreate a blank
864    >>> from Crypto.Util import number
865    >>> base64.b64encode(number.long_to_bytes(blank4.blind_factor))
866    '...'
867
868    """
869
870    def __init__(self, **kwargs):
871        CurrencyBase.__init__(self, **kwargs)
872        self.blind_factor = kwargs.get('blind_factor', None)
873
874    def generateSerial(self): 
875        from crypto import _r as Random
876        if self.serial:
877            raise Exception('Cannot generate a new serial when the serial is set.')
878       
879        self.serial = Random.getRandomString(128)
880
881    def blind_blank(self, cdd, mint_key, blind_factor=None):
882        """Returns the blinded value of the hash of the coin for signing."""
883
884        if self.blind_factor: # we only allow blind_factors to be passed in.
885            raise Exception('CurrencyBlank already has a blind factor')
886
887        self.blinding = cdd.issuer_cipher_suite.blinding(mint_key.public_key)
888        hashing = cdd.issuer_cipher_suite.hashing(self.content_part())
889
890        # Note: we pass in blind_factor. If None, the blind_factor will be automaticall generated
891        self.blind_value, self.blind_factor = self.blinding.blind(hashing.digest(), blind_factor)
892        return self.blind_value
893
894    def unblind_signature(self, signature):
895        """Returns the unblinded value of the blinded signature."""
896           
897        return self.blinding.unblind(signature)
898
899    def setBlind(self, blind_alg, key, blind_factor=None):
900        """setBlind is used to set the state of self.blinding without performing the blinding.
901        It is similar to blind_blank except that it does not perform any blinding itself.
902        """
903       
904        if blind_factor:
905            self.blind_factor = blind_factor
906
907        self.blinding = blind_alg(key, self.blind_factor)
908       
909    def newCoin(self, signature, currency_description_document=None, mint_key=None):
910        """Returns a coin using the unblinded signature.
911        Performs tests if currency_description_document and mint_key are provided.
912        """
913        coin = CurrencyCoin(standard_identifier = self.standard_identifier, 
914                            currency_identifier = self.currency_identifier, 
915                            denomination = self.denomination, 
916                            key_identifier = self.key_identifier,
917                            serial = self.serial,
918                            signature = signature)
919       
920       
921        # if only one is provided, we have an error. Purposefully use an 'or' for the test to get an exception later
922        if currency_description_document or mint_key:
923            if not coin.validate_with_CDD_and_MintKey(currency_description_document, 
924                                                         mint_key):
925                raise BlankError('New coin does not validate!')
926       
927        return coin
928
929class BlankError(Exception):
930    """BlankError is when a Blank cannot become a coin."""
931    pass
932       
933class CurrencyCoin(CurrencyBase):
934    """The CurrencyCoin.
935
936    >>> from tests import CDD, CDD_private, keys512, addSignature, mintKeys
937    >>> import copy
938    >>> from calendar import timegm
939
940    >>> private = keys512[0]
941    >>> public = private.newPublicKeyPair()
942                         
943    >>> hash_alg = CDD.issuer_cipher_suite.hashing
944    >>> sign_alg = CDD.issuer_cipher_suite.signing
945    >>> blind_alg = CDD.issuer_cipher_suite.blinding
946   
947    >>> mintKey = mintKeys[0]
948
949    >>> signature = 'HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdpYhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A=='
950
951    >>> coin = CurrencyCoin(standard_identifier=CDD.standard_identifier,
952    ...                     currency_identifier=CDD.currency_identifier,
953    ...                     denomination='1',
954    ...                     key_identifier=mintKey.key_identifier,
955    ...                     serial='abcdefghijklmnopqrstuvwxyz',
956    ...                     signature=base64.b64decode(signature))
957
958    >>> coin.toJson()
959    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["denomination","1"],["key_identifier","sj17RxE1hfO06+oTgBs9Z7xLut/3NN+nHJbXSJYTks0="],["serial","YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="],["signature","HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdpYhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A=="]]'
960
961    >>> coin.content_part()
962    '[["standard_identifier","http://opencoin.org/OpenCoinProtocol/1.0"],["currency_identifier","http://opencent.net/OpenCent"],["denomination","1"],["key_identifier","sj17RxE1hfO06+oTgBs9Z7xLut/3NN+nHJbXSJYTks0="],["serial","YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo="]]'
963
964    >>> coin2 = CurrencyCoin().fromJson(coin.toJson())
965    >>> coin2 == coin
966    True
967
968    >>> coin.validate_with_CDD_and_MintKey(CDD, mintKey)
969    True
970
971    Check that a bad signature will fail
972    >>> coin3 = copy.deepcopy(coin)
973    >>> coin3.signature = 'foobar'
974    >>> coin3.validate_with_CDD_and_MintKey(CDD, mintKey)
975    False
976
977    """
978   
979    def __init__(self, **kwargs):
980        CurrencyBase.__init__(self, **kwargs)
981        self.jsontext = None
982        self.signature = kwargs.get('signature')
983
984    # base64 encoding and decoding is hardcoded here in to/fromJson
985    def toJson(self,extranames=1):
986        if extranames:
987            if self.jsontext:
988                return self.jsontext
989            else:
990                data = self.toPython(forcesig=1)
991                self.jsontext = json.write(data)
992                return self.jsontext
993        else:       
994            return json.write(self.toPython(nosig=1))
995
996    def fromJson(self,text):
997        data = json.read(text)
998        if len(data) == len(self.fields) + 1 and data[-1][0] == 'signature':
999            self.jsontext = text
1000        return self.fromPython(data)
1001
1002    def toPython(self,forcesig=None,nosig=None):
1003        data = CurrencyBase.toPython(self)
1004        if forcesig or (not nosig and self.signature):
1005            data.append(('signature',base64.b64encode(self.signature)))
1006        return data           
1007
1008    def fromPython(self,data):
1009        CurrencyBase.fromPython(self,data)
1010        if len(data) == len(self.fields) + 1 and data[-1][0] == 'signature':
1011            self.signature = base64.b64decode(data[-1][1])
1012        return self           
1013
1014
1015    def validate_with_CDD_and_MintKey(self, currency_description_document, mint_key):
1016
1017        if not CurrencyBase.validate_with_CDD_and_MintKey(self, currency_description_document, mint_key):
1018            return False
1019
1020        key = mint_key.public_key
1021        signer = currency_description_document.issuer_cipher_suite.signing(key)
1022        hasher = currency_description_document.issuer_cipher_suite.hashing(self.content_part())
1023
1024        if not signer.verify(hasher.digest(), self.signature):
1025            return False
1026
1027        return True
1028
1029
1030if __name__ == "__main__":
1031    import doctest
1032    doctest.testmod(optionflags=doctest.ELLIPSIS) 
Note: See TracBrowser for help on using the browser.