changes proposed by Aaron Helsinger and GENI
[sfa.git] / sfa / trust / certificate.py
1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
10 #
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
13 #
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS 
21 # IN THE WORK.
22 #----------------------------------------------------------------------
23
24 ##
25 # SFA uses two crypto libraries: pyOpenSSL and M2Crypto to implement
26 # the necessary crypto functionality. Ideally just one of these libraries
27 # would be used, but unfortunately each of these libraries is independently
28 # lacking. The pyOpenSSL library is missing many necessary functions, and
29 # the M2Crypto library has crashed inside of some of the functions. The
30 # design decision is to use pyOpenSSL whenever possible as it seems more
31 # stable, and only use M2Crypto for those functions that are not possible
32 # in pyOpenSSL.
33 #
34 # This module exports two classes: Keypair and Certificate.
35 ##
36 #
37
38 import functools
39 import os
40 import tempfile
41 import base64
42 from tempfile import mkstemp
43
44 from OpenSSL import crypto
45 import M2Crypto
46 from M2Crypto import X509
47
48 from sfa.util.faults import CertExpired, CertMissingParent, CertNotSignedByParent
49 from sfa.util.sfalogging import logger
50
51 # this tends to generate quite some logs for little or no value
52 debug_verify_chain = False
53
54 glo_passphrase_callback = None
55
56 ##
57 # A global callback may be implemented for requesting passphrases from the
58 # user. The function will be called with three arguments:
59 #
60 #    keypair_obj: the keypair object that is calling the passphrase
61 #    string: the string containing the private key that's being loaded
62 #    x: unknown, appears to be 0, comes from pyOpenSSL and/or m2crypto
63 #
64 # The callback should return a string containing the passphrase.
65
66 def set_passphrase_callback(callback_func):
67     global glo_passphrase_callback
68
69     glo_passphrase_callback = callback_func
70
71 ##
72 # Sets a fixed passphrase.
73
74 def set_passphrase(passphrase):
75     set_passphrase_callback( lambda k,s,x: passphrase )
76
77 ##
78 # Check to see if a passphrase works for a particular private key string.
79 # Intended to be used by passphrase callbacks for input validation.
80
81 def test_passphrase(string, passphrase):
82     try:
83         crypto.load_privatekey(crypto.FILETYPE_PEM, string, (lambda x: passphrase))
84         return True
85     except:
86         return False
87
88 def convert_public_key(key):
89     keyconvert_path = "/usr/bin/keyconvert.py"
90     if not os.path.isfile(keyconvert_path):
91         raise IOError, "Could not find keyconvert in %s" % keyconvert_path
92
93     # we can only convert rsa keys
94     if "ssh-dss" in key:
95         raise Exception, "keyconvert: dss keys are not supported"
96
97     (ssh_f, ssh_fn) = tempfile.mkstemp()
98     ssl_fn = tempfile.mktemp()
99     os.write(ssh_f, key)
100     os.close(ssh_f)
101
102     cmd = keyconvert_path + " " + ssh_fn + " " + ssl_fn
103     os.system(cmd)
104
105     # this check leaves the temporary file containing the public key so
106     # that it can be expected to see why it failed.
107     # TODO: for production, cleanup the temporary files
108     if not os.path.exists(ssl_fn):
109         raise Exception, "keyconvert: generated certificate not found. keyconvert may have failed."
110
111     k = Keypair()
112     try:
113         k.load_pubkey_from_file(ssl_fn)
114         return k
115     except:
116         logger.log_exc("convert_public_key caught exception")
117         raise
118     finally:
119         # remove the temporary files
120         if os.path.exists(ssh_fn):
121             os.remove(ssh_fn)
122         if os.path.exists(ssl_fn):
123             os.remove(ssl_fn)
124
125 ##
126 # Public-private key pairs are implemented by the Keypair class.
127 # A Keypair object may represent both a public and private key pair, or it
128 # may represent only a public key (this usage is consistent with OpenSSL).
129
130 class Keypair:
131     key = None       # public/private keypair
132     m2key = None     # public key (m2crypto format)
133
134     ##
135     # Creates a Keypair object
136     # @param create If create==True, creates a new public/private key and
137     #     stores it in the object
138     # @param string If string!=None, load the keypair from the string (PEM)
139     # @param filename If filename!=None, load the keypair from the file
140
141     def __init__(self, create=False, string=None, filename=None):
142         if create:
143             self.create()
144         if string:
145             self.load_from_string(string)
146         if filename:
147             self.load_from_file(filename)
148
149     ##
150     # Create a RSA public/private key pair and store it inside the keypair object
151
152     def create(self):
153         self.key = crypto.PKey()
154         self.key.generate_key(crypto.TYPE_RSA, 2048)
155
156     ##
157     # Save the private key to a file
158     # @param filename name of file to store the keypair in
159
160     def save_to_file(self, filename):
161         open(filename, 'w').write(self.as_pem())
162         self.filename=filename
163
164     ##
165     # Load the private key from a file. Implicity the private key includes the public key.
166
167     def load_from_file(self, filename):
168         self.filename=filename
169         buffer = open(filename, 'r').read()
170         self.load_from_string(buffer)
171
172     ##
173     # Load the private key from a string. Implicitly the private key includes the public key.
174
175     def load_from_string(self, string):
176         if glo_passphrase_callback:
177             self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string, functools.partial(glo_passphrase_callback, self, string) )
178             self.m2key = M2Crypto.EVP.load_key_string(string, functools.partial(glo_passphrase_callback, self, string) )
179         else:
180             self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string)
181             self.m2key = M2Crypto.EVP.load_key_string(string)
182
183     ##
184     #  Load the public key from a string. No private key is loaded.
185
186     def load_pubkey_from_file(self, filename):
187         # load the m2 public key
188         m2rsakey = M2Crypto.RSA.load_pub_key(filename)
189         self.m2key = M2Crypto.EVP.PKey()
190         self.m2key.assign_rsa(m2rsakey)
191
192         # create an m2 x509 cert
193         m2name = M2Crypto.X509.X509_Name()
194         m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0)
195         m2x509 = M2Crypto.X509.X509()
196         m2x509.set_pubkey(self.m2key)
197         m2x509.set_serial_number(0)
198         m2x509.set_issuer_name(m2name)
199         m2x509.set_subject_name(m2name)
200         ASN1 = M2Crypto.ASN1.ASN1_UTCTIME()
201         ASN1.set_time(500)
202         m2x509.set_not_before(ASN1)
203         m2x509.set_not_after(ASN1)
204         # x509v3 so it can have extensions
205         # prob not necc since this cert itself is junk but still...
206         m2x509.set_version(2)
207         junk_key = Keypair(create=True)
208         m2x509.sign(pkey=junk_key.get_m2_pkey(), md="sha1")
209
210         # convert the m2 x509 cert to a pyopenssl x509
211         m2pem = m2x509.as_pem()
212         pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem)
213
214         # get the pyopenssl pkey from the pyopenssl x509
215         self.key = pyx509.get_pubkey()
216         self.filename=filename
217
218     ##
219     # Load the public key from a string. No private key is loaded.
220
221     def load_pubkey_from_string(self, string):
222         (f, fn) = tempfile.mkstemp()
223         os.write(f, string)
224         os.close(f)
225         self.load_pubkey_from_file(fn)
226         os.remove(fn)
227
228     ##
229     # Return the private key in PEM format.
230
231     def as_pem(self):
232         return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
233
234     ##
235     # Return an M2Crypto key object
236
237     def get_m2_pkey(self):
238         if not self.m2key:
239             self.m2key = M2Crypto.EVP.load_key_string(self.as_pem())
240         return self.m2key
241
242     ##
243     # Returns a string containing the public key represented by this object.
244
245     def get_pubkey_string(self):
246         m2pkey = self.get_m2_pkey()
247         return base64.b64encode(m2pkey.as_der())
248
249     ##
250     # Return an OpenSSL pkey object
251
252     def get_openssl_pkey(self):
253         return self.key
254
255     ##
256     # Given another Keypair object, return TRUE if the two keys are the same.
257
258     def is_same(self, pkey):
259         return self.as_pem() == pkey.as_pem()
260
261     def sign_string(self, data):
262         k = self.get_m2_pkey()
263         k.sign_init()
264         k.sign_update(data)
265         return base64.b64encode(k.sign_final())
266
267     def verify_string(self, data, sig):
268         k = self.get_m2_pkey()
269         k.verify_init()
270         k.verify_update(data)
271         return M2Crypto.m2.verify_final(k.ctx, base64.b64decode(sig), k.pkey)
272
273     def compute_hash(self, value):
274         return self.sign_string(str(value))
275
276     # only informative
277     def get_filename(self):
278         return getattr(self,'filename',None)
279
280     def dump (self, *args, **kwargs):
281         print self.dump_string(*args, **kwargs)
282
283     def dump_string (self):
284         result=""
285         result += "KEYPAIR: pubkey=%40s..."%self.get_pubkey_string()
286         filename=self.get_filename()
287         if filename: result += "Filename %s\n"%filename
288         return result
289
290 ##
291 # The certificate class implements a general purpose X509 certificate, making
292 # use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds
293 # several addition features, such as the ability to maintain a chain of
294 # parent certificates, and storage of application-specific data.
295 #
296 # Certificates include the ability to maintain a chain of parents. Each
297 # certificate includes a pointer to it's parent certificate. When loaded
298 # from a file or a string, the parent chain will be automatically loaded.
299 # When saving a certificate to a file or a string, the caller can choose
300 # whether to save the parent certificates as well.
301
302 class Certificate:
303     digest = "sha256"
304
305 #    x509 = None
306 #    issuerKey = None
307 #    issuerSubject = None
308 #    parent = None
309     isCA = None # will be a boolean once set
310
311     separator="-----parent-----"
312
313     ##
314     # Create a certificate object.
315     #
316     # @param lifeDays life of cert in days - default is 1825==5 years
317     # @param create If create==True, then also create a blank X509 certificate.
318     # @param subject If subject!=None, then create a blank certificate and set
319     #     it's subject name.
320     # @param string If string!=None, load the certficate from the string.
321     # @param filename If filename!=None, load the certficiate from the file.
322     # @param isCA If !=None, set whether this cert is for a CA
323
324     def __init__(self, lifeDays=1825, create=False, subject=None, string=None, filename=None, isCA=None):
325         # these used to be defined in the class !
326         self.x509 = None
327         self.issuerKey = None
328         self.issuerSubject = None
329         self.parent = None
330
331         self.data = {}
332         if create or subject:
333             self.create(lifeDays)
334         if subject:
335             self.set_subject(subject)
336         if string:
337             self.load_from_string(string)
338         if filename:
339             self.load_from_file(filename)
340
341         # Set the CA bit if a value was supplied
342         if isCA != None:
343             self.set_is_ca(isCA)
344
345     # Create a blank X509 certificate and store it in this object.
346
347     def create(self, lifeDays=1825):
348         self.x509 = crypto.X509()
349         # FIXME: Use different serial #s
350         self.x509.set_serial_number(3)
351         self.x509.gmtime_adj_notBefore(0) # 0 means now
352         self.x509.gmtime_adj_notAfter(lifeDays*60*60*24) # five years is default
353         self.x509.set_version(2) # x509v3 so it can have extensions
354
355
356     ##
357     # Given a pyOpenSSL X509 object, store that object inside of this
358     # certificate object.
359
360     def load_from_pyopenssl_x509(self, x509):
361         self.x509 = x509
362
363     ##
364     # Load the certificate from a string
365
366     def load_from_string(self, string):
367         # if it is a chain of multiple certs, then split off the first one and
368         # load it (support for the ---parent--- tag as well as normal chained certs)
369
370         if string is None or string.strip() == "":
371             logger.warn("Empty string in load_from_string")
372             return
373
374         string = string.strip()
375         
376         # If it's not in proper PEM format, wrap it
377         if string.count('-----BEGIN CERTIFICATE') == 0:
378             string = '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----' % string
379
380         # If there is a PEM cert in there, but there is some other text first
381         # such as the text of the certificate, skip the text
382         beg = string.find('-----BEGIN CERTIFICATE')
383         if beg > 0:
384             # skipping over non cert beginning                                                                                                              
385             string = string[beg:]
386
387         parts = []
388
389         if string.count('-----BEGIN CERTIFICATE-----') > 1 and \
390                string.count(Certificate.separator) == 0:
391             parts = string.split('-----END CERTIFICATE-----',1)
392             parts[0] += '-----END CERTIFICATE-----'
393         else:
394             parts = string.split(Certificate.separator, 1)
395
396         self.x509 = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0])
397
398         if self.x509 is None:
399             logger.warn("Loaded from string but cert is None: %s" % string)
400
401         # if there are more certs, then create a parent and let the parent load
402         # itself from the remainder of the string
403         if len(parts) > 1 and parts[1] != '':
404             self.parent = self.__class__()
405             self.parent.load_from_string(parts[1])
406
407     ##
408     # Load the certificate from a file
409
410     def load_from_file(self, filename):
411         file = open(filename)
412         string = file.read()
413         self.load_from_string(string)
414         self.filename=filename
415
416     ##
417     # Save the certificate to a string.
418     #
419     # @param save_parents If save_parents==True, then also save the parent certificates.
420
421     def save_to_string(self, save_parents=True):
422         if self.x509 is None:
423             logger.warn("None cert in certificate.save_to_string")
424             return ""
425         string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.x509)
426         if save_parents and self.parent:
427             string = string + self.parent.save_to_string(save_parents)
428         return string
429
430     ##
431     # Save the certificate to a file.
432     # @param save_parents If save_parents==True, then also save the parent certificates.
433
434     def save_to_file(self, filename, save_parents=True, filep=None):
435         string = self.save_to_string(save_parents=save_parents)
436         if filep:
437             f = filep
438         else:
439             f = open(filename, 'w')
440         f.write(string)
441         f.close()
442         self.filename=filename
443
444     ##
445     # Save the certificate to a random file in /tmp/
446     # @param save_parents If save_parents==True, then also save the parent certificates.
447     def save_to_random_tmp_file(self, save_parents=True):
448         fp, filename = mkstemp(suffix='cert', text=True)
449         fp = os.fdopen(fp, "w")
450         self.save_to_file(filename, save_parents=True, filep=fp)
451         return filename
452
453     ##
454     # Sets the issuer private key and name
455     # @param key Keypair object containing the private key of the issuer
456     # @param subject String containing the name of the issuer
457     # @param cert (optional) Certificate object containing the name of the issuer
458
459     def set_issuer(self, key, subject=None, cert=None):
460         self.issuerKey = key
461         if subject:
462             # it's a mistake to use subject and cert params at the same time
463             assert(not cert)
464             if isinstance(subject, dict) or isinstance(subject, str):
465                 req = crypto.X509Req()
466                 reqSubject = req.get_subject()
467                 if (isinstance(subject, dict)):
468                     for key in reqSubject.keys():
469                         setattr(reqSubject, key, subject[key])
470                 else:
471                     setattr(reqSubject, "CN", subject)
472                 subject = reqSubject
473                 # subject is not valid once req is out of scope, so save req
474                 self.issuerReq = req
475         if cert:
476             # if a cert was supplied, then get the subject from the cert
477             subject = cert.x509.get_subject()
478         assert(subject)
479         self.issuerSubject = subject
480
481     ##
482     # Get the issuer name
483
484     def get_issuer(self, which="CN"):
485         x = self.x509.get_issuer()
486         return getattr(x, which)
487
488     ##
489     # Set the subject name of the certificate
490
491     def set_subject(self, name):
492         req = crypto.X509Req()
493         subj = req.get_subject()
494         if (isinstance(name, dict)):
495             for key in name.keys():
496                 setattr(subj, key, name[key])
497         else:
498             setattr(subj, "CN", name)
499         self.x509.set_subject(subj)
500
501     ##
502     # Get the subject name of the certificate
503
504     def get_subject(self, which="CN"):
505         x = self.x509.get_subject()
506         return getattr(x, which)
507
508     ##
509     # Get a pretty-print subject name of the certificate
510     # let's try to make this a little more usable as is makes logs hairy
511     # FIXME: Consider adding 'urn:publicid' and 'uuid' back for GENI?
512     pretty_fields = ['email']
513     def filter_chunk(self, chunk):
514         for field in self.pretty_fields:
515             if field in chunk:
516                 return " "+chunk
517
518     def pretty_cert(self):
519         message = "[Cert."
520         x = self.x509.get_subject()
521         ou = getattr(x, "OU")
522         if ou: message += " OU: {}".format(ou)
523         cn = getattr(x, "CN")
524         if cn: message += " CN: {}".format(cn)
525         data = self.get_data(field='subjectAltName')
526         if data:
527             message += " SubjectAltName:"
528             counter = 0
529             filtered = [self.filter_chunk(chunk) for chunk in data.split()]
530             message += " ".join( [f for f in filtered if f])
531             omitted = len ([f for f in filtered if not f])
532             if omitted:
533                 message += "..+{} omitted".format(omitted)
534         message += "]"
535         return message
536
537     ##
538     # Get the public key of the certificate.
539     #
540     # @param key Keypair object containing the public key
541
542     def set_pubkey(self, key):
543         assert(isinstance(key, Keypair))
544         self.x509.set_pubkey(key.get_openssl_pkey())
545
546     ##
547     # Get the public key of the certificate.
548     # It is returned in the form of a Keypair object.
549
550     def get_pubkey(self):
551         m2x509 = X509.load_cert_string(self.save_to_string())
552         pkey = Keypair()
553         pkey.key = self.x509.get_pubkey()
554         pkey.m2key = m2x509.get_pubkey()
555         return pkey
556
557     def set_intermediate_ca(self, val):
558         return self.set_is_ca(val)
559
560     # Set whether this cert is for a CA. All signers and only signers should be CAs.
561     # The local member starts unset, letting us check that you only set it once
562     # @param val Boolean indicating whether this cert is for a CA
563     def set_is_ca(self, val):
564         if val is None:
565             return
566
567         if self.isCA != None:
568             # Can't double set properties
569             raise Exception, "Cannot set basicConstraints CA:?? more than once. Was %s, trying to set as %s" % (self.isCA, val)
570
571         self.isCA = val
572         if val:
573             self.add_extension('basicConstraints', 1, 'CA:TRUE')
574         else:
575             self.add_extension('basicConstraints', 1, 'CA:FALSE')
576
577
578
579     ##
580     # Add an X509 extension to the certificate. Add_extension can only be called
581     # once for a particular extension name, due to limitations in the underlying
582     # library.
583     #
584     # @param name string containing name of extension
585     # @param value string containing value of the extension
586
587     def add_extension(self, name, critical, value):
588         oldExtVal = None
589         try:
590             oldExtVal = self.get_extension(name)
591         except:
592             # M2Crypto LookupError when the extension isn't there (yet)
593             pass
594
595         # This code limits you from adding the extension with the same value
596         # The method comment says you shouldn't do this with the same name
597         # But actually it (m2crypto) appears to allow you to do this.
598         if oldExtVal and oldExtVal == value:
599             # don't add this extension again
600             # just do nothing as here
601             return
602         # FIXME: What if they are trying to set with a different value?
603         # Is this ever OK? Or should we raise an exception?
604 #        elif oldExtVal:
605 #            raise "Cannot add extension %s which had val %s with new val %s" % (name, oldExtVal, value)
606
607         ext = crypto.X509Extension (name, critical, value)
608         self.x509.add_extensions([ext])
609
610     ##
611     # Get an X509 extension from the certificate
612
613     def get_extension(self, name):
614
615         if name is None:
616             return None
617
618         certstr = self.save_to_string()
619         if certstr is None or certstr == "":
620             return None
621         # pyOpenSSL does not have a way to get extensions
622         m2x509 = X509.load_cert_string(certstr)
623         if m2x509 is None:
624             logger.warn("No cert loaded in get_extension")
625             return None
626         if m2x509.get_ext(name) is None:
627             return None
628         value = m2x509.get_ext(name).get_value()
629
630         return value
631
632     ##
633     # Set_data is a wrapper around add_extension. It stores the parameter str in
634     # the X509 subject_alt_name extension. Set_data can only be called once, due
635     # to limitations in the underlying library.
636
637     def set_data(self, str, field='subjectAltName'):
638         # pyOpenSSL only allows us to add extensions, so if we try to set the
639         # same extension more than once, it will not work
640         if self.data.has_key(field):
641             raise "Cannot set ", field, " more than once"
642         self.data[field] = str
643         self.add_extension(field, 0, str)
644
645     ##
646     # Return the data string that was previously set with set_data
647
648     def get_data(self, field='subjectAltName'):
649         if self.data.has_key(field):
650             return self.data[field]
651
652         try:
653             uri = self.get_extension(field)
654             self.data[field] = uri
655         except LookupError:
656             return None
657
658         return self.data[field]
659
660     ##
661     # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer().
662
663     def sign(self):
664         logger.debug('certificate.sign')
665         assert self.x509 != None
666         assert self.issuerSubject != None
667         assert self.issuerKey != None
668         self.x509.set_issuer(self.issuerSubject)
669         self.x509.sign(self.issuerKey.get_openssl_pkey(), self.digest)
670
671     ##
672     # Verify the authenticity of a certificate.
673     # @param pkey is a Keypair object representing a public key. If Pkey
674     #     did not sign the certificate, then an exception will be thrown.
675
676     def verify(self, pkey):
677         # pyOpenSSL does not have a way to verify signatures
678         m2x509 = X509.load_cert_string(self.save_to_string())
679         m2pkey = pkey.get_m2_pkey()
680         # verify it
681         return m2x509.verify(m2pkey)
682
683         # XXX alternatively, if openssl has been patched, do the much simpler:
684         # try:
685         #   self.x509.verify(pkey.get_openssl_key())
686         #   return 1
687         # except:
688         #   return 0
689
690     ##
691     # Return True if pkey is identical to the public key that is contained in the certificate.
692     # @param pkey Keypair object
693
694     def is_pubkey(self, pkey):
695         return self.get_pubkey().is_same(pkey)
696
697     ##
698     # Given a certificate cert, verify that this certificate was signed by the
699     # public key contained in cert. Throw an exception otherwise.
700     #
701     # @param cert certificate object
702
703     def is_signed_by_cert(self, cert):
704         k = cert.get_pubkey()
705         result = self.verify(k)
706         return result
707
708     ##
709     # Set the parent certficiate.
710     #
711     # @param p certificate object.
712
713     def set_parent(self, p):
714         self.parent = p
715
716     ##
717     # Return the certificate object of the parent of this certificate.
718
719     def get_parent(self):
720         return self.parent
721
722     ##
723     # Verification examines a chain of certificates to ensure that each parent
724     # signs the child, and that some certificate in the chain is signed by a
725     # trusted certificate.
726     #
727     # Verification is a basic recursion: <pre>
728     #     if this_certificate was signed by trusted_certs:
729     #         return
730     #     else
731     #         return verify_chain(parent, trusted_certs)
732     # </pre>
733     #
734     # At each recursion, the parent is tested to ensure that it did sign the
735     # child. If a parent did not sign a child, then an exception is thrown. If
736     # the bottom of the recursion is reached and the certificate does not match
737     # a trusted root, then an exception is thrown.
738     # Also require that parents are CAs.
739     #
740     # @param Trusted_certs is a list of certificates that are trusted.
741     #
742
743     def verify_chain(self, trusted_certs = None):
744         # Verify a chain of certificates. Each certificate must be signed by
745         # the public key contained in it's parent. The chain is recursed
746         # until a certificate is found that is signed by a trusted root.
747
748         # verify expiration time
749         if self.x509.has_expired():
750             if debug_verify_chain:
751                 logger.debug("verify_chain: NO, Certificate %s has expired" % self.pretty_cert())
752             raise CertExpired(self.pretty_cert(), "client cert")
753
754         # if this cert is signed by a trusted_cert, then we are set
755         for trusted_cert in trusted_certs:
756             if self.is_signed_by_cert(trusted_cert):
757                 # verify expiration of trusted_cert ?
758                 if not trusted_cert.x509.has_expired():
759                     if debug_verify_chain:
760                         logger.debug("verify_chain: YES. Cert %s signed by trusted cert %s"%(
761                             self.pretty_cert(), trusted_cert.pretty_cert()))
762                     return trusted_cert
763                 else:
764                     if debug_verify_chain:
765                         logger.debug("verify_chain: NO. Cert %s is signed by trusted_cert %s, but that signer is expired..."%(
766                             self.pretty_cert(),trusted_cert.pretty_cert()))
767                     raise CertExpired(self.pretty_cert()," signer trusted_cert %s"%trusted_cert.pretty_cert())
768
769         # if there is no parent, then no way to verify the chain
770         if not self.parent:
771             if debug_verify_chain:
772                 logger.debug("verify_chain: NO. %s has no parent and issuer %s is not in %d trusted roots"%\
773                              (self.pretty_cert(), self.get_issuer(), len(trusted_certs)))
774             raise CertMissingParent(self.pretty_cert() + \
775                                     ": Issuer %s is not one of the %d trusted roots, and cert has no parent." %\
776                                     (self.get_issuer(), len(trusted_certs)))
777
778         # if it wasn't signed by the parent...
779         if not self.is_signed_by_cert(self.parent):
780             if debug_verify_chain:
781                 logger.debug("verify_chain: NO. %s is not signed by parent %s, but by %s"%\
782                              (self.pretty_cert(),
783                               self.parent.pretty_cert(),
784                               self.get_issuer()))
785             raise CertNotSignedByParent("%s: Parent %s, issuer %s"\
786                                             % (self.pretty_cert(),
787                                                self.parent.pretty_cert(),
788                                                self.get_issuer()))
789
790         # Confirm that the parent is a CA. Only CAs can be trusted as
791         # signers.
792         # Note that trusted roots are not parents, so don't need to be
793         # CAs.
794         # Ugly - cert objects aren't parsed so we need to read the
795         # extension and hope there are no other basicConstraints
796         if not self.parent.isCA and not (self.parent.get_extension('basicConstraints') == 'CA:TRUE'):
797             logger.warn("verify_chain: cert %s's parent %s is not a CA" % \
798                             (self.pretty_cert(), self.parent.pretty_cert()))
799             raise CertNotSignedByParent("%s: Parent %s not a CA" % (self.pretty_cert(),
800                                                                     self.parent.pretty_cert()))
801
802         # if the parent isn't verified...
803         if debug_verify_chain:
804             logger.debug("verify_chain: .. %s, -> verifying parent %s"%\
805                          (self.pretty_cert(),self.parent.pretty_cert()))
806         self.parent.verify_chain(trusted_certs)
807
808         return
809
810     ### more introspection
811     def get_extensions(self):
812         # pyOpenSSL does not have a way to get extensions
813         triples = []
814         m2x509 = X509.load_cert_string(self.save_to_string())
815         nb_extensions = m2x509.get_ext_count()
816         logger.debug("X509 had %d extensions"%nb_extensions)
817         for i in range(nb_extensions):
818             ext=m2x509.get_ext_at(i)
819             triples.append( (ext.get_name(), ext.get_value(), ext.get_critical(),) )
820         return triples
821
822     def get_data_names(self):
823         return self.data.keys()
824
825     def get_all_datas (self):
826         triples = self.get_extensions()
827         for name in self.get_data_names():
828             triples.append( (name,self.get_data(name),'data',) )
829         return triples
830
831     # only informative
832     def get_filename(self):
833         return getattr(self,'filename',None)
834
835     def dump (self, *args, **kwargs):
836         print self.dump_string(*args, **kwargs)
837
838     def dump_string (self,show_extensions=False):
839         result = ""
840         result += "CERTIFICATE for %s\n"%self.pretty_cert()
841         result += "Issued by %s\n"%self.get_issuer()
842         filename=self.get_filename()
843         if filename: result += "Filename %s\n"%filename
844         if show_extensions:
845             all_datas = self.get_all_datas()
846             result += " has %d extensions/data attached"%len(all_datas)
847             for (n, v, c) in all_datas:
848                 if c=='data':
849                     result += "   data: %s=%s\n"%(n,v)
850                 else:
851                     result += "    ext: %s (crit=%s)=<<<%s>>>\n"%(n,c,v)
852         return result