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