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