ff6534e083fc23537469f9cd483ab743deb25e5b
[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(
178                 crypto.FILETYPE_PEM, string, functools.partial(glo_passphrase_callback, self, string))
179             self.m2key = M2Crypto.EVP.load_key_string(
180                 string, functools.partial(glo_passphrase_callback, self, string))
181         else:
182             self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string)
183             self.m2key = M2Crypto.EVP.load_key_string(string)
184
185     ##
186     #  Load the public key from a string. No private key is loaded.
187
188     def load_pubkey_from_file(self, filename):
189         # load the m2 public key
190         m2rsakey = M2Crypto.RSA.load_pub_key(filename)
191         self.m2key = M2Crypto.EVP.PKey()
192         self.m2key.assign_rsa(m2rsakey)
193
194         # create an m2 x509 cert
195         m2name = M2Crypto.X509.X509_Name()
196         m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0)
197         m2x509 = M2Crypto.X509.X509()
198         m2x509.set_pubkey(self.m2key)
199         m2x509.set_serial_number(0)
200         m2x509.set_issuer_name(m2name)
201         m2x509.set_subject_name(m2name)
202         ASN1 = M2Crypto.ASN1.ASN1_UTCTIME()
203         ASN1.set_time(500)
204         m2x509.set_not_before(ASN1)
205         m2x509.set_not_after(ASN1)
206         # x509v3 so it can have extensions
207         # prob not necc since this cert itself is junk but still...
208         m2x509.set_version(2)
209         junk_key = Keypair(create=True)
210         m2x509.sign(pkey=junk_key.get_m2_pubkey(), md="sha1")
211
212         # convert the m2 x509 cert to a pyopenssl x509
213         m2pem = m2x509.as_pem()
214         pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem)
215
216         # get the pyopenssl pkey from the pyopenssl x509
217         self.key = pyx509.get_pubkey()
218         self.filename=filename
219
220     ##
221     # Load the public key from a string. No private key is loaded.
222
223     def load_pubkey_from_string(self, string):
224         (f, fn) = tempfile.mkstemp()
225         os.write(f, string)
226         os.close(f)
227         self.load_pubkey_from_file(fn)
228         os.remove(fn)
229
230     ##
231     # Return the private key in PEM format.
232
233     def as_pem(self):
234         return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
235
236     ##
237     # Return an M2Crypto key object
238
239     def get_m2_pubkey(self):
240         if not self.m2key:
241             self.m2key = M2Crypto.EVP.load_key_string(self.as_pem())
242         return self.m2key
243
244     ##
245     # Returns a string containing the public key represented by this object.
246
247     def get_pubkey_string(self):
248         m2pkey = self.get_m2_pubkey()
249         return base64.b64encode(m2pkey.as_der())
250
251     ##
252     # Return an OpenSSL pkey object
253
254     def get_openssl_pkey(self):
255         return self.key
256
257     ##
258     # Given another Keypair object, return TRUE if the two keys are the same.
259
260     def is_same(self, pkey):
261         return self.as_pem() == pkey.as_pem()
262
263     def sign_string(self, data):
264         k = self.get_m2_pubkey()
265         k.sign_init()
266         k.sign_update(data)
267         return base64.b64encode(k.sign_final())
268
269     def verify_string(self, data, sig):
270         k = self.get_m2_pubkey()
271         k.verify_init()
272         k.verify_update(data)
273         return M2Crypto.m2.verify_final(k.ctx, base64.b64decode(sig), k.pkey)
274
275     def compute_hash(self, value):
276         return self.sign_string(str(value))
277
278     # only informative
279     def get_filename(self):
280         return getattr(self,'filename',None)
281
282     def dump (self, *args, **kwargs):
283         print self.dump_string(*args, **kwargs)
284
285     def dump_string (self):
286         result=""
287         result += "KEYPAIR: pubkey=%40s..."%self.get_pubkey_string()
288         filename=self.get_filename()
289         if filename: result += "Filename %s\n"%filename
290         return result
291
292 ##
293 # The certificate class implements a general purpose X509 certificate, making
294 # use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds
295 # several addition features, such as the ability to maintain a chain of
296 # parent certificates, and storage of application-specific data.
297 #
298 # Certificates include the ability to maintain a chain of parents. Each
299 # certificate includes a pointer to it's parent certificate. When loaded
300 # from a file or a string, the parent chain will be automatically loaded.
301 # When saving a certificate to a file or a string, the caller can choose
302 # whether to save the parent certificates as well.
303
304 class Certificate:
305     digest = "sha256"
306
307 #    x509 = None
308 #    issuerKey = None
309 #    issuerSubject = None
310 #    parent = None
311     isCA = None # will be a boolean once set
312
313     separator="-----parent-----"
314
315     ##
316     # Create a certificate object.
317     #
318     # @param lifeDays life of cert in days - default is 1825==5 years
319     # @param create If create==True, then also create a blank X509 certificate.
320     # @param subject If subject!=None, then create a blank certificate and set
321     #     it's subject name.
322     # @param string If string!=None, load the certficate from the string.
323     # @param filename If filename!=None, load the certficiate from the file.
324     # @param isCA If !=None, set whether this cert is for a CA
325
326     def __init__(self, lifeDays=1825, create=False, subject=None, string=None, filename=None, isCA=None):
327         # these used to be defined in the class !
328         self.x509 = None
329         self.issuerKey = None
330         self.issuerSubject = None
331         self.parent = None
332
333         self.data = {}
334         if create or subject:
335             self.create(lifeDays)
336         if subject:
337             self.set_subject(subject)
338         if string:
339             self.load_from_string(string)
340         if filename:
341             self.load_from_file(filename)
342
343         # Set the CA bit if a value was supplied
344         if isCA != None:
345             self.set_is_ca(isCA)
346
347     # Create a blank X509 certificate and store it in this object.
348
349     def create(self, lifeDays=1825):
350         self.x509 = crypto.X509()
351         # FIXME: Use different serial #s
352         self.x509.set_serial_number(3)
353         self.x509.gmtime_adj_notBefore(0) # 0 means now
354         self.x509.gmtime_adj_notAfter(lifeDays*60*60*24) # five years is default
355         self.x509.set_version(2) # x509v3 so it can have extensions
356
357
358     ##
359     # Given a pyOpenSSL X509 object, store that object inside of this
360     # certificate object.
361
362     def load_from_pyopenssl_x509(self, x509):
363         self.x509 = x509
364
365     ##
366     # Load the certificate from a string
367
368     def load_from_string(self, string):
369         # if it is a chain of multiple certs, then split off the first one and
370         # load it (support for the ---parent--- tag as well as normal chained certs)
371
372         if string is None or string.strip() == "":
373             logger.warn("Empty string in load_from_string")
374             return
375
376         string = string.strip()
377         
378         # If it's not in proper PEM format, wrap it
379         if string.count('-----BEGIN CERTIFICATE') == 0:
380             string = '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----' % string
381
382         # If there is a PEM cert in there, but there is some other text first
383         # such as the text of the certificate, skip the text
384         beg = string.find('-----BEGIN CERTIFICATE')
385         if beg > 0:
386             # skipping over non cert beginning                                                                                                              
387             string = string[beg:]
388
389         parts = []
390
391         if string.count('-----BEGIN CERTIFICATE-----') > 1 and \
392                string.count(Certificate.separator) == 0:
393             parts = string.split('-----END CERTIFICATE-----',1)
394             parts[0] += '-----END CERTIFICATE-----'
395         else:
396             parts = string.split(Certificate.separator, 1)
397
398         self.x509 = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0])
399
400         if self.x509 is None:
401             logger.warn("Loaded from string but cert is None: %s" % string)
402
403         # if there are more certs, then create a parent and let the parent load
404         # itself from the remainder of the string
405         if len(parts) > 1 and parts[1] != '':
406             self.parent = self.__class__()
407             self.parent.load_from_string(parts[1])
408
409     ##
410     # Load the certificate from a file
411
412     def load_from_file(self, filename):
413         file = open(filename)
414         string = file.read()
415         self.load_from_string(string)
416         self.filename=filename
417
418     ##
419     # Save the certificate to a string.
420     #
421     # @param save_parents If save_parents==True, then also save the parent certificates.
422
423     def save_to_string(self, save_parents=True):
424         if self.x509 is None:
425             logger.warn("None cert in certificate.save_to_string")
426             return ""
427         string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.x509)
428         if save_parents and self.parent:
429             string = string + self.parent.save_to_string(save_parents)
430         return string
431
432     ##
433     # Save the certificate to a file.
434     # @param save_parents If save_parents==True, then also save the parent certificates.
435
436     def save_to_file(self, filename, save_parents=True, filep=None):
437         string = self.save_to_string(save_parents=save_parents)
438         if filep:
439             f = filep
440         else:
441             f = open(filename, 'w')
442         f.write(string)
443         f.close()
444         self.filename=filename
445
446     ##
447     # Save the certificate to a random file in /tmp/
448     # @param save_parents If save_parents==True, then also save the parent certificates.
449     def save_to_random_tmp_file(self, save_parents=True):
450         fp, filename = mkstemp(suffix='cert', text=True)
451         fp = os.fdopen(fp, "w")
452         self.save_to_file(filename, save_parents=True, filep=fp)
453         return filename
454
455     ##
456     # Sets the issuer private key and name
457     # @param key Keypair object containing the private key of the issuer
458     # @param subject String containing the name of the issuer
459     # @param cert (optional) Certificate object containing the name of the issuer
460
461     def set_issuer(self, key, subject=None, cert=None):
462         self.issuerKey = key
463         if subject:
464             # it's a mistake to use subject and cert params at the same time
465             assert(not cert)
466             if isinstance(subject, dict) or isinstance(subject, str):
467                 req = crypto.X509Req()
468                 reqSubject = req.get_subject()
469                 if (isinstance(subject, dict)):
470                     for key in reqSubject.keys():
471                         setattr(reqSubject, key, subject[key])
472                 else:
473                     setattr(reqSubject, "CN", subject)
474                 subject = reqSubject
475                 # subject is not valid once req is out of scope, so save req
476                 self.issuerReq = req
477         if cert:
478             # if a cert was supplied, then get the subject from the cert
479             subject = cert.x509.get_subject()
480         assert(subject)
481         self.issuerSubject = subject
482
483     ##
484     # Get the issuer name
485
486     def get_issuer(self, which="CN"):
487         x = self.x509.get_issuer()
488         return getattr(x, which)
489
490     ##
491     # Set the subject name of the certificate
492
493     def set_subject(self, name):
494         req = crypto.X509Req()
495         subj = req.get_subject()
496         if (isinstance(name, dict)):
497             for key in name.keys():
498                 setattr(subj, key, name[key])
499         else:
500             setattr(subj, "CN", name)
501         self.x509.set_subject(subj)
502
503     ##
504     # Get the subject name of the certificate
505
506     def get_subject(self, which="CN"):
507         x = self.x509.get_subject()
508         return getattr(x, which)
509
510     ##
511     # Get a pretty-print subject name of the certificate
512     # let's try to make this a little more usable as is makes logs hairy
513     # FIXME: Consider adding 'urn:publicid' and 'uuid' back for GENI?
514     pretty_fields = ['email']
515     def filter_chunk(self, chunk):
516         for field in self.pretty_fields:
517             if field in chunk:
518                 return " "+chunk
519
520     def pretty_cert(self):
521         message = "[Cert."
522         x = self.x509.get_subject()
523         ou = getattr(x, "OU")
524         if ou: message += " OU: {}".format(ou)
525         cn = getattr(x, "CN")
526         if cn: message += " CN: {}".format(cn)
527         data = self.get_data(field='subjectAltName')
528         if data:
529             message += " SubjectAltName:"
530             counter = 0
531             filtered = [self.filter_chunk(chunk) for chunk in data.split()]
532             message += " ".join( [f for f in filtered if f])
533             omitted = len ([f for f in filtered if not f])
534             if omitted:
535                 message += "..+{} omitted".format(omitted)
536         message += "]"
537         return message
538
539     ##
540     # Get the public key of the certificate.
541     #
542     # @param key Keypair object containing the public key
543
544     def set_pubkey(self, key):
545         assert(isinstance(key, Keypair))
546         self.x509.set_pubkey(key.get_openssl_pkey())
547
548     ##
549     # Get the public key of the certificate.
550     # It is returned in the form of a Keypair object.
551
552     def get_pubkey(self):
553         m2x509 = X509.load_cert_string(self.save_to_string())
554         pkey = Keypair()
555         pkey.key = self.x509.get_pubkey()
556         pkey.m2key = m2x509.get_pubkey()
557         return pkey
558
559     def set_intermediate_ca(self, val):
560         return self.set_is_ca(val)
561
562     # Set whether this cert is for a CA. All signers and only signers should be CAs.
563     # The local member starts unset, letting us check that you only set it once
564     # @param val Boolean indicating whether this cert is for a CA
565     def set_is_ca(self, val):
566         if val is None:
567             return
568
569         if self.isCA != None:
570             # Can't double set properties
571             raise Exception, "Cannot set basicConstraints CA:?? more than once. Was %s, trying to set as %s" % (self.isCA, val)
572
573         self.isCA = val
574         if val:
575             self.add_extension('basicConstraints', 1, 'CA:TRUE')
576         else:
577             self.add_extension('basicConstraints', 1, 'CA:FALSE')
578
579
580
581     ##
582     # Add an X509 extension to the certificate. Add_extension can only be called
583     # once for a particular extension name, due to limitations in the underlying
584     # library.
585     #
586     # @param name string containing name of extension
587     # @param value string containing value of the extension
588
589     def add_extension(self, name, critical, value):
590         oldExtVal = None
591         try:
592             oldExtVal = self.get_extension(name)
593         except:
594             # M2Crypto LookupError when the extension isn't there (yet)
595             pass
596
597         # This code limits you from adding the extension with the same value
598         # The method comment says you shouldn't do this with the same name
599         # But actually it (m2crypto) appears to allow you to do this.
600         if oldExtVal and oldExtVal == value:
601             # don't add this extension again
602             # just do nothing as here
603             return
604         # FIXME: What if they are trying to set with a different value?
605         # Is this ever OK? Or should we raise an exception?
606 #        elif oldExtVal:
607 #            raise "Cannot add extension %s which had val %s with new val %s" % (name, oldExtVal, value)
608
609         ext = crypto.X509Extension (name, critical, value)
610         self.x509.add_extensions([ext])
611
612     ##
613     # Get an X509 extension from the certificate
614
615     def get_extension(self, name):
616
617         if name is None:
618             return None
619
620         certstr = self.save_to_string()
621         if certstr is None or certstr == "":
622             return None
623         # pyOpenSSL does not have a way to get extensions
624         m2x509 = X509.load_cert_string(certstr)
625         if m2x509 is None:
626             logger.warn("No cert loaded in get_extension")
627             return None
628         if m2x509.get_ext(name) is None:
629             return None
630         value = m2x509.get_ext(name).get_value()
631
632         return value
633
634     ##
635     # Set_data is a wrapper around add_extension. It stores the parameter str in
636     # the X509 subject_alt_name extension. Set_data can only be called once, due
637     # to limitations in the underlying library.
638
639     def set_data(self, str, field='subjectAltName'):
640         # pyOpenSSL only allows us to add extensions, so if we try to set the
641         # same extension more than once, it will not work
642         if self.data.has_key(field):
643             raise "Cannot set ", field, " more than once"
644         self.data[field] = str
645         self.add_extension(field, 0, str)
646
647     ##
648     # Return the data string that was previously set with set_data
649
650     def get_data(self, field='subjectAltName'):
651         if self.data.has_key(field):
652             return self.data[field]
653
654         try:
655             uri = self.get_extension(field)
656             self.data[field] = uri
657         except LookupError:
658             return None
659
660         return self.data[field]
661
662     ##
663     # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer().
664
665     def sign(self):
666         logger.debug('certificate.sign')
667         assert self.x509 != None
668         assert self.issuerSubject != None
669         assert self.issuerKey != None
670         self.x509.set_issuer(self.issuerSubject)
671         self.x509.sign(self.issuerKey.get_openssl_pkey(), self.digest)
672
673     ##
674     # Verify the authenticity of a certificate.
675     # @param pkey is a Keypair object representing a public key. If Pkey
676     #     did not sign the certificate, then an exception will be thrown.
677
678     def verify(self, pubkey):
679         # pyOpenSSL does not have a way to verify signatures
680         m2x509 = X509.load_cert_string(self.save_to_string())
681         m2pubkey = pubkey.get_m2_pubkey()
682         # verify it
683         # verify returns -1 or 0 on failure depending on how serious the
684         # error conditions are
685         return m2x509.verify(m2pubkey) == 1
686
687         # XXX alternatively, if openssl has been patched, do the much simpler:
688         # try:
689         #   self.x509.verify(pkey.get_openssl_key())
690         #   return 1
691         # except:
692         #   return 0
693
694     ##
695     # Return True if pkey is identical to the public key that is contained in the certificate.
696     # @param pkey Keypair object
697
698     def is_pubkey(self, pkey):
699         return self.get_pubkey().is_same(pkey)
700
701     ##
702     # Given a certificate cert, verify that this certificate was signed by the
703     # public key contained in cert. Throw an exception otherwise.
704     #
705     # @param cert certificate object
706
707     def is_signed_by_cert(self, cert):
708         k = cert.get_pubkey()
709         result = self.verify(k)
710         return result
711
712     ##
713     # Set the parent certficiate.
714     #
715     # @param p certificate object.
716
717     def set_parent(self, p):
718         self.parent = p
719
720     ##
721     # Return the certificate object of the parent of this certificate.
722
723     def get_parent(self):
724         return self.parent
725
726     ##
727     # Verification examines a chain of certificates to ensure that each parent
728     # signs the child, and that some certificate in the chain is signed by a
729     # trusted certificate.
730     #
731     # Verification is a basic recursion: <pre>
732     #     if this_certificate was signed by trusted_certs:
733     #         return
734     #     else
735     #         return verify_chain(parent, trusted_certs)
736     # </pre>
737     #
738     # At each recursion, the parent is tested to ensure that it did sign the
739     # child. If a parent did not sign a child, then an exception is thrown. If
740     # the bottom of the recursion is reached and the certificate does not match
741     # a trusted root, then an exception is thrown.
742     # Also require that parents are CAs.
743     #
744     # @param Trusted_certs is a list of certificates that are trusted.
745     #
746
747     def verify_chain(self, trusted_certs = None):
748         # Verify a chain of certificates. Each certificate must be signed by
749         # the public key contained in it's parent. The chain is recursed
750         # until a certificate is found that is signed by a trusted root.
751
752         # verify expiration time
753         if self.x509.has_expired():
754             if debug_verify_chain:
755                 logger.debug("verify_chain: NO, Certificate %s has expired" % self.pretty_cert())
756             raise CertExpired(self.pretty_cert(), "client cert")
757
758         # if this cert is signed by a trusted_cert, then we are set
759         for trusted_cert in trusted_certs:
760             if self.is_signed_by_cert(trusted_cert):
761                 # verify expiration of trusted_cert ?
762                 if not trusted_cert.x509.has_expired():
763                     if debug_verify_chain:
764                         logger.debug("verify_chain: YES. Cert %s signed by trusted cert %s"%(
765                             self.pretty_cert(), trusted_cert.pretty_cert()))
766                     return trusted_cert
767                 else:
768                     if debug_verify_chain:
769                         logger.debug("verify_chain: NO. Cert %s is signed by trusted_cert %s, but that signer is expired..."%(
770                             self.pretty_cert(),trusted_cert.pretty_cert()))
771                     raise CertExpired(self.pretty_cert()," signer trusted_cert %s"%trusted_cert.pretty_cert())
772
773         # if there is no parent, then no way to verify the chain
774         if not self.parent:
775             if debug_verify_chain:
776                 logger.debug("verify_chain: NO. %s has no parent and issuer %s is not in %d trusted roots"%\
777                              (self.pretty_cert(), self.get_issuer(), len(trusted_certs)))
778             raise CertMissingParent(self.pretty_cert() + \
779                                     ": Issuer %s is not one of the %d trusted roots, and cert has no parent." %\
780                                     (self.get_issuer(), len(trusted_certs)))
781
782         # if it wasn't signed by the parent...
783         if not self.is_signed_by_cert(self.parent):
784             if debug_verify_chain:
785                 logger.debug("verify_chain: NO. %s is not signed by parent %s, but by %s"%\
786                              (self.pretty_cert(),
787                               self.parent.pretty_cert(),
788                               self.get_issuer()))
789             raise CertNotSignedByParent("%s: Parent %s, issuer %s"\
790                                             % (self.pretty_cert(),
791                                                self.parent.pretty_cert(),
792                                                self.get_issuer()))
793
794         # Confirm that the parent is a CA. Only CAs can be trusted as
795         # signers.
796         # Note that trusted roots are not parents, so don't need to be
797         # CAs.
798         # Ugly - cert objects aren't parsed so we need to read the
799         # extension and hope there are no other basicConstraints
800         if not self.parent.isCA and not (self.parent.get_extension('basicConstraints') == 'CA:TRUE'):
801             logger.warn("verify_chain: cert %s's parent %s is not a CA" % \
802                             (self.pretty_cert(), self.parent.pretty_cert()))
803             raise CertNotSignedByParent("%s: Parent %s not a CA" % (self.pretty_cert(),
804                                                                     self.parent.pretty_cert()))
805
806         # if the parent isn't verified...
807         if debug_verify_chain:
808             logger.debug("verify_chain: .. %s, -> verifying parent %s"%\
809                          (self.pretty_cert(),self.parent.pretty_cert()))
810         self.parent.verify_chain(trusted_certs)
811
812         return
813
814     ### more introspection
815     def get_extensions(self):
816         # pyOpenSSL does not have a way to get extensions
817         triples = []
818         m2x509 = X509.load_cert_string(self.save_to_string())
819         nb_extensions = m2x509.get_ext_count()
820         logger.debug("X509 had %d extensions"%nb_extensions)
821         for i in range(nb_extensions):
822             ext=m2x509.get_ext_at(i)
823             triples.append( (ext.get_name(), ext.get_value(), ext.get_critical(),) )
824         return triples
825
826     def get_data_names(self):
827         return self.data.keys()
828
829     def get_all_datas (self):
830         triples = self.get_extensions()
831         for name in self.get_data_names():
832             triples.append( (name,self.get_data(name),'data',) )
833         return triples
834
835     # only informative
836     def get_filename(self):
837         return getattr(self,'filename',None)
838
839     def dump (self, *args, **kwargs):
840         print self.dump_string(*args, **kwargs)
841
842     def dump_string (self,show_extensions=False):
843         result = ""
844         result += "CERTIFICATE for %s\n"%self.pretty_cert()
845         result += "Issued by %s\n"%self.get_issuer()
846         filename=self.get_filename()
847         if filename: result += "Filename %s\n"%filename
848         if show_extensions:
849             all_datas = self.get_all_datas()
850             result += " has %d extensions/data attached"%len(all_datas)
851             for (n, v, c) in all_datas:
852                 if c=='data':
853                     result += "   data: %s=%s\n"%(n,v)
854                 else:
855                     result += "    ext: %s (crit=%s)=<<<%s>>>\n"%(n,c,v)
856         return result