install keyconvert to /usr/bin
[sfa.git] / sfa / trust / certificate.py
1 ##
2 # Geniwrapper uses two crypto libraries: pyOpenSSL and M2Crypto to implement
3 # the necessary crypto functionality. Ideally just one of these libraries
4 # would be used, but unfortunately each of these libraries is independently
5 # lacking. The pyOpenSSL library is missing many necessary functions, and
6 # the M2Crypto library has crashed inside of some of the functions. The
7 # design decision is to use pyOpenSSL whenever possible as it seems more
8 # stable, and only use M2Crypto for those functions that are not possible
9 # in pyOpenSSL.
10 #
11 # This module exports two classes: Keypair and Certificate.
12 ##
13 #
14 ### $Id$
15 ### $URL$
16 #
17
18 import os
19 import tempfile
20 import base64
21 from OpenSSL import crypto
22 import M2Crypto
23 from M2Crypto import X509
24 from M2Crypto import EVP
25
26 from sfa.util.faults import *
27
28 def convert_public_key(key):
29     keyconvert_path = "/usr/bin/keyconvert"
30     if not os.path.isfile(keyconvert_path):
31         raise IOError, "Could not find keyconvert in %s" % keyconvert_path
32
33     # we can only convert rsa keys 
34     if "ssh-dss" in key:
35         print "XXX: DSA key encountered, ignoring"
36         return None
37     
38     (ssh_f, ssh_fn) = tempfile.mkstemp()
39     ssl_fn = tempfile.mktemp()
40     os.write(ssh_f, key)
41     os.close(ssh_f)
42
43     cmd = keyconvert_path + " " + ssh_fn + " " + ssl_fn
44     os.system(cmd)
45
46     # this check leaves the temporary file containing the public key so
47     # that it can be expected to see why it failed.
48     # TODO: for production, cleanup the temporary files
49     if not os.path.exists(ssl_fn):
50         report.trace("  failed to convert key from " + ssh_fn + " to " + ssl_fn)
51         return None
52
53     k = Keypair()
54     try:
55         k.load_pubkey_from_file(ssl_fn)
56     except:
57         print "XXX: Error while converting key: ", key_str
58         k = None
59
60     # remove the temporary files
61     os.remove(ssh_fn)
62     os.remove(ssl_fn)
63
64     return k
65
66 ##
67 # Public-private key pairs are implemented by the Keypair class.
68 # A Keypair object may represent both a public and private key pair, or it
69 # may represent only a public key (this usage is consistent with OpenSSL).
70
71 class Keypair:
72    key = None       # public/private keypair
73    m2key = None     # public key (m2crypto format)
74
75    ##
76    # Creates a Keypair object
77    # @param create If create==True, creates a new public/private key and
78    #     stores it in the object
79    # @param string If string!=None, load the keypair from the string (PEM)
80    # @param filename If filename!=None, load the keypair from the file
81
82    def __init__(self, create=False, string=None, filename=None):
83       if create:
84          self.create()
85       if string:
86          self.load_from_string(string)
87       if filename:
88          self.load_from_file(filename)
89
90    ##
91    # Create a RSA public/private key pair and store it inside the keypair object
92
93    def create(self):
94       self.key = crypto.PKey()
95       self.key.generate_key(crypto.TYPE_RSA, 1024)
96
97    ##
98    # Save the private key to a file
99    # @param filename name of file to store the keypair in
100
101    def save_to_file(self, filename):
102       open(filename, 'w').write(self.as_pem())
103
104    ##
105    # Load the private key from a file. Implicity the private key includes the public key.
106
107    def load_from_file(self, filename):
108       buffer = open(filename, 'r').read()
109       self.load_from_string(buffer)
110
111    ##
112    # Load the private key from a string. Implicitly the private key includes the public key.
113
114    def load_from_string(self, string):
115       self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string)
116       self.m2key = M2Crypto.EVP.load_key_string(string)
117
118    ##
119    #  Load the public key from a string. No private key is loaded. 
120
121    def load_pubkey_from_file(self, filename):
122       # load the m2 public key
123       m2rsakey = M2Crypto.RSA.load_pub_key(filename)
124       self.m2key = M2Crypto.EVP.PKey()
125       self.m2key.assign_rsa(m2rsakey)
126
127       # create an m2 x509 cert
128       m2name = M2Crypto.X509.X509_Name()
129       m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0)
130       m2x509 = M2Crypto.X509.X509()
131       m2x509.set_pubkey(self.m2key)
132       m2x509.set_serial_number(0)
133       m2x509.set_issuer_name(m2name)
134       m2x509.set_subject_name(m2name)
135       ASN1 = M2Crypto.ASN1.ASN1_UTCTIME()
136       ASN1.set_time(500)
137       m2x509.set_not_before(ASN1)
138       m2x509.set_not_after(ASN1)
139       junk_key = Keypair(create=True)
140       m2x509.sign(pkey=junk_key.get_m2_pkey(), md="sha1")
141
142       # convert the m2 x509 cert to a pyopenssl x509
143       m2pem = m2x509.as_pem()
144       pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem)
145
146       # get the pyopenssl pkey from the pyopenssl x509
147       self.key = pyx509.get_pubkey()
148
149    ##
150    # Load the public key from a string. No private key is loaded.
151
152    def load_pubkey_from_string(self, string):
153       (f, fn) = tempfile.mkstemp()
154       os.write(f, string)
155       os.close(f)
156       self.load_pubkey_from_file(fn)
157       os.remove(fn)
158
159    ##
160    # Return the private key in PEM format.
161
162    def as_pem(self):
163       return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
164
165    def get_m2_pkey(self):
166       if not self.m2key:
167          self.m2key = M2Crypto.EVP.load_key_string(self.as_pem())
168       return self.m2key
169
170    ##
171    # Return an OpenSSL pkey object
172
173    def get_openssl_pkey(self):
174       return self.key
175
176    ##
177    # Given another Keypair object, return TRUE if the two keys are the same.
178
179    def is_same(self, pkey):
180       return self.as_pem() == pkey.as_pem()
181
182    def sign_string(self, data):
183       k = self.get_m2_pkey()
184       k.sign_init()
185       k.sign_update(data)
186       return base64.b64encode(k.sign_final())
187
188    def verify_string(self, data, sig):
189       k = self.get_m2_pkey()
190       k.verify_init()
191       k.verify_update(data)
192       return M2Crypto.m2.verify_final(k.ctx, base64.b64decode(sig), k.pkey)
193
194 ##
195 # The certificate class implements a general purpose X509 certificate, making
196 # use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds
197 # several addition features, such as the ability to maintain a chain of
198 # parent certificates, and storage of application-specific data.
199 #
200 # Certificates include the ability to maintain a chain of parents. Each
201 # certificate includes a pointer to it's parent certificate. When loaded
202 # from a file or a string, the parent chain will be automatically loaded.
203 # When saving a certificate to a file or a string, the caller can choose
204 # whether to save the parent certificates as well.
205
206 class Certificate:
207    digest = "md5"
208
209    data = None
210    cert = None
211    issuerKey = None
212    issuerSubject = None
213    parent = None
214
215    separator="-----parent-----"
216
217    ##
218    # Create a certificate object.
219    #
220    # @param create If create==True, then also create a blank X509 certificate.
221    # @param subject If subject!=None, then create a blank certificate and set
222    #     it's subject name.
223    # @param string If string!=None, load the certficate from the string.
224    # @param filename If filename!=None, load the certficiate from the file.
225
226    def __init__(self, create=False, subject=None, string=None, filename=None):
227        if create or subject:
228            self.create()
229        if subject:
230            self.set_subject(subject)
231        if string:
232            self.load_from_string(string)
233        if filename:
234            self.load_from_file(filename)
235
236    ##
237    # Create a blank X509 certificate and store it in this object.
238
239    def create(self):
240        self.cert = crypto.X509()
241        self.cert.set_serial_number(1)
242        self.cert.gmtime_adj_notBefore(0)
243        self.cert.gmtime_adj_notAfter(60*60*24*365*5) # five years
244
245    ##
246    # Given a pyOpenSSL X509 object, store that object inside of this
247    # certificate object.
248
249    def load_from_pyopenssl_x509(self, x509):
250        self.cert = x509
251
252    ##
253    # Load the certificate from a string
254
255    def load_from_string(self, string):
256        # if it is a chain of multiple certs, then split off the first one and
257        # load it
258        parts = string.split(Certificate.separator, 1)
259        self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0])
260
261        # if there are more certs, then create a parent and let the parent load
262        # itself from the remainder of the string
263        if len(parts) > 1:
264            self.parent = self.__class__()
265            self.parent.load_from_string(parts[1])
266
267    ##
268    # Load the certificate from a file
269
270    def load_from_file(self, filename):
271        file = open(filename)
272        string = file.read()
273        self.load_from_string(string)
274
275    ##
276    # Save the certificate to a string.
277    #
278    # @param save_parents If save_parents==True, then also save the parent certificates.
279
280    def save_to_string(self, save_parents=False):
281        string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
282        if save_parents and self.parent:
283           string = string + Certificate.separator + self.parent.save_to_string(save_parents)
284        return string
285
286    ##
287    # Save the certificate to a file.
288    # @param save_parents If save_parents==True, then also save the parent certificates.
289
290    def save_to_file(self, filename, save_parents=False):
291        string = self.save_to_string(save_parents=save_parents)
292        open(filename, 'w').write(string)
293
294    ##
295    # Sets the issuer private key and name
296    # @param key Keypair object containing the private key of the issuer
297    # @param subject String containing the name of the issuer
298    # @param cert (optional) Certificate object containing the name of the issuer
299
300    def set_issuer(self, key, subject=None, cert=None):
301        self.issuerKey = key
302        if subject:
303           # it's a mistake to use subject and cert params at the same time
304           assert(not cert)
305           if isinstance(subject, dict) or isinstance(subject, str):
306              req = crypto.X509Req()
307              reqSubject = req.get_subject()
308              if (isinstance(subject, dict)):
309                 for key in reqSubject.keys():
310                     setattr(reqSubject, key, name[key])
311              else:
312                 setattr(reqSubject, "CN", subject)
313              subject = reqSubject
314              # subject is not valid once req is out of scope, so save req
315              self.issuerReq = req
316        if cert:
317           # if a cert was supplied, then get the subject from the cert
318           subject = cert.cert.get_issuer()
319        assert(subject)
320        self.issuerSubject = subject
321
322    ##
323    # Get the issuer name
324
325    def get_issuer(self, which="CN"):
326        x = self.cert.get_issuer()
327        return getattr(x, which)
328
329    ##
330    # Set the subject name of the certificate
331
332    def set_subject(self, name):
333        req = crypto.X509Req()
334        subj = req.get_subject()
335        if (isinstance(name, dict)):
336            for key in name.keys():
337                setattr(subj, key, name[key])
338        else:
339            setattr(subj, "CN", name)
340        self.cert.set_subject(subj)
341    ##
342    # Get the subject name of the certificate
343
344    def get_subject(self, which="CN"):
345        x = self.cert.get_subject()
346        return getattr(x, which)
347
348    ##
349    # Get the public key of the certificate.
350    #
351    # @param key Keypair object containing the public key
352
353    def set_pubkey(self, key):
354        assert(isinstance(key, Keypair))
355        self.cert.set_pubkey(key.get_openssl_pkey())
356
357    ##
358    # Get the public key of the certificate.
359    # It is returned in the form of a Keypair object.
360
361    def get_pubkey(self):
362        m2x509 = X509.load_cert_string(self.save_to_string())
363        pkey = Keypair()
364        pkey.key = self.cert.get_pubkey()
365        pkey.m2key = m2x509.get_pubkey()
366        return pkey
367
368    ##
369    # Add an X509 extension to the certificate. Add_extension can only be called
370    # once for a particular extension name, due to limitations in the underlying
371    # library.
372    #
373    # @param name string containing name of extension
374    # @param value string containing value of the extension
375
376    def add_extension(self, name, critical, value):
377        ext = crypto.X509Extension (name, critical, value)
378        self.cert.add_extensions([ext])
379
380    ##
381    # Get an X509 extension from the certificate
382
383    def get_extension(self, name):
384        # pyOpenSSL does not have a way to get extensions
385        m2x509 = X509.load_cert_string(self.save_to_string())
386        value = m2x509.get_ext(name).get_value()
387        return value
388
389    ##
390    # Set_data is a wrapper around add_extension. It stores the parameter str in
391    # the X509 subject_alt_name extension. Set_data can only be called once, due
392    # to limitations in the underlying library.
393
394    def set_data(self, str):
395        # pyOpenSSL only allows us to add extensions, so if we try to set the
396        # same extension more than once, it will not work
397        if self.data != None:
398           raise "cannot set subjectAltName more than once"
399        self.data = str
400        self.add_extension("subjectAltName", 0, "URI:http://" + str)
401
402    ##
403    # Return the data string that was previously set with set_data
404
405    def get_data(self):
406        if self.data:
407            return self.data
408
409        try:
410            uri = self.get_extension("subjectAltName")
411        except LookupError:
412            self.data = None
413            return self.data
414
415        if not uri.startswith("URI:http://"):
416            raise "bad encoding in subjectAltName"
417        self.data = uri[11:]
418        return self.data
419
420    ##
421    # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer().
422
423    def sign(self):
424        assert self.cert != None
425        assert self.issuerSubject != None
426        assert self.issuerKey != None
427        self.cert.set_issuer(self.issuerSubject)
428        self.cert.sign(self.issuerKey.get_openssl_pkey(), self.digest)
429
430     ##
431     # Verify the authenticity of a certificate.
432     # @param pkey is a Keypair object representing a public key. If Pkey
433     #     did not sign the certificate, then an exception will be thrown.
434
435    def verify(self, pkey):
436        # pyOpenSSL does not have a way to verify signatures
437        m2x509 = X509.load_cert_string(self.save_to_string())
438        m2pkey = pkey.get_m2_pkey()
439        # verify it
440        return m2x509.verify(m2pkey)
441
442        # XXX alternatively, if openssl has been patched, do the much simpler:
443        # try:
444        #   self.cert.verify(pkey.get_openssl_key())
445        #   return 1
446        # except:
447        #   return 0
448
449    ##
450    # Return True if pkey is identical to the public key that is contained in the certificate.
451    # @param pkey Keypair object
452
453    def is_pubkey(self, pkey):
454        return self.get_pubkey().is_same(pkey)
455
456    ##
457    # Given a certificate cert, verify that this certificate was signed by the
458    # public key contained in cert. Throw an exception otherwise.
459    #
460    # @param cert certificate object
461
462    def is_signed_by_cert(self, cert):
463        k = cert.get_pubkey()
464        result = self.verify(k)
465        return result
466
467    ##
468    # Set the parent certficiate.
469    #
470    # @param p certificate object.
471
472    def set_parent(self, p):
473         self.parent = p
474
475    ##
476    # Return the certificate object of the parent of this certificate.
477
478    def get_parent(self):
479         return self.parent
480
481    ##
482    # Verification examines a chain of certificates to ensure that each parent
483    # signs the child, and that some certificate in the chain is signed by a
484    # trusted certificate.
485    #
486    # Verification is a basic recursion: <pre>
487    #     if this_certificate was signed by trusted_certs:
488    #         return
489    #     else
490    #         return verify_chain(parent, trusted_certs)
491    # </pre>
492    #
493    # At each recursion, the parent is tested to ensure that it did sign the
494    # child. If a parent did not sign a child, then an exception is thrown. If
495    # the bottom of the recursion is reached and the certificate does not match
496    # a trusted root, then an exception is thrown.
497    #
498    # @param Trusted_certs is a list of certificates that are trusted.
499    #
500
501    def verify_chain(self, trusted_certs = None):
502         # Verify a chain of certificates. Each certificate must be signed by
503         # the public key contained in it's parent. The chain is recursed
504         # until a certificate is found that is signed by a trusted root.
505
506         # TODO: verify expiration time
507
508         # if this cert is signed by a trusted_cert, then we are set
509         for trusted_cert in trusted_certs:
510             # TODO: verify expiration of trusted_cert ?
511             if self.is_signed_by_cert(trusted_cert):
512                 #print self.get_subject(), "is signed by a root"
513                 return
514
515         # if there is no parent, then no way to verify the chain
516         if not self.parent:
517             #print self.get_subject(), "has no parent"
518             raise CertMissingParent(self.get_subject())
519
520         # if it wasn't signed by the parent...
521         if not self.is_signed_by_cert(self.parent):
522             #print self.get_subject(), "is not signed by parent"
523             return CertNotSignedByParent(self.get_subject())
524
525         # if the parent isn't verified...
526         self.parent.verify_chain(trusted_certs)
527
528         return