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
11 # This module exports two classes: Keypair and Certificate.
21 from OpenSSL import crypto
23 from M2Crypto import X509
24 from M2Crypto import EVP
26 from sfa.util.faults import *
28 def convert_public_key(key):
29 # find the keyconvert program
30 from sfa.util.config import Config
32 keyconvert = 'keyconvert'
34 default_path = "/usr/share/keyconvert/" + keyconvert
35 cwd = os.path.dirname(os.path.abspath(__file__))
36 alt_path = os.sep.join(cwd.split(os.sep)[:-1] + ['keyconvert', 'keyconvert'])
37 geni_path = config.basepath + os.sep + "keyconvert/keyconvert"
38 files = [default_path, alt_path, geni_path]
40 if os.path.isfile(path):
46 raise Exception, "Could not find keyconvert in " + ", ".join(files)
48 # we can only convert rsa keys
50 print "XXX: DSA key encountered, ignoring"
53 (ssh_f, ssh_fn) = tempfile.mkstemp()
54 ssl_fn = tempfile.mktemp()
58 if not os.path.exists(keyconvert_fn):
59 report.trace(" keyconvet utility " + str(keyconvert_fn) + "does not exist")
62 cmd = keyconvert_fn + " " + ssh_fn + " " + ssl_fn
65 # this check leaves the temporary file containing the public key so
66 # that it can be expected to see why it failed.
67 # TODO: for production, cleanup the temporary files
68 if not os.path.exists(ssl_fn):
69 report.trace(" failed to convert key from " + ssh_fn + " to " + ssl_fn)
74 k.load_pubkey_from_file(ssl_fn)
76 print "XXX: Error while converting key: ", key_str
79 # remove the temporary files
86 # Public-private key pairs are implemented by the Keypair class.
87 # A Keypair object may represent both a public and private key pair, or it
88 # may represent only a public key (this usage is consistent with OpenSSL).
91 key = None # public/private keypair
92 m2key = None # public key (m2crypto format)
95 # Creates a Keypair object
96 # @param create If create==True, creates a new public/private key and
97 # stores it in the object
98 # @param string If string!=None, load the keypair from the string (PEM)
99 # @param filename If filename!=None, load the keypair from the file
101 def __init__(self, create=False, string=None, filename=None):
105 self.load_from_string(string)
107 self.load_from_file(filename)
110 # Create a RSA public/private key pair and store it inside the keypair object
113 self.key = crypto.PKey()
114 self.key.generate_key(crypto.TYPE_RSA, 1024)
117 # Save the private key to a file
118 # @param filename name of file to store the keypair in
120 def save_to_file(self, filename):
121 open(filename, 'w').write(self.as_pem())
124 # Load the private key from a file. Implicity the private key includes the public key.
126 def load_from_file(self, filename):
127 buffer = open(filename, 'r').read()
128 self.load_from_string(buffer)
131 # Load the private key from a string. Implicitly the private key includes the public key.
133 def load_from_string(self, string):
134 self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string)
135 self.m2key = M2Crypto.EVP.load_key_string(string)
138 # Load the public key from a string. No private key is loaded.
140 def load_pubkey_from_file(self, filename):
141 # load the m2 public key
142 m2rsakey = M2Crypto.RSA.load_pub_key(filename)
143 self.m2key = M2Crypto.EVP.PKey()
144 self.m2key.assign_rsa(m2rsakey)
146 # create an m2 x509 cert
147 m2name = M2Crypto.X509.X509_Name()
148 m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0)
149 m2x509 = M2Crypto.X509.X509()
150 m2x509.set_pubkey(self.m2key)
151 m2x509.set_serial_number(0)
152 m2x509.set_issuer_name(m2name)
153 m2x509.set_subject_name(m2name)
154 ASN1 = M2Crypto.ASN1.ASN1_UTCTIME()
156 m2x509.set_not_before(ASN1)
157 m2x509.set_not_after(ASN1)
158 junk_key = Keypair(create=True)
159 m2x509.sign(pkey=junk_key.get_m2_pkey(), md="sha1")
161 # convert the m2 x509 cert to a pyopenssl x509
162 m2pem = m2x509.as_pem()
163 pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem)
165 # get the pyopenssl pkey from the pyopenssl x509
166 self.key = pyx509.get_pubkey()
169 # Load the public key from a string. No private key is loaded.
171 def load_pubkey_from_string(self, string):
172 (f, fn) = tempfile.mkstemp()
175 self.load_pubkey_from_file(fn)
179 # Return the private key in PEM format.
182 return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
184 def get_m2_pkey(self):
186 self.m2key = M2Crypto.EVP.load_key_string(self.as_pem())
190 # Return an OpenSSL pkey object
192 def get_openssl_pkey(self):
196 # Given another Keypair object, return TRUE if the two keys are the same.
198 def is_same(self, pkey):
199 return self.as_pem() == pkey.as_pem()
201 def sign_string(self, data):
202 k = self.get_m2_pkey()
205 return base64.b64encode(k.sign_final())
207 def verify_string(self, data, sig):
208 k = self.get_m2_pkey()
210 k.verify_update(data)
211 return M2Crypto.m2.verify_final(k.ctx, base64.b64decode(sig), k.pkey)
214 # The certificate class implements a general purpose X509 certificate, making
215 # use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds
216 # several addition features, such as the ability to maintain a chain of
217 # parent certificates, and storage of application-specific data.
219 # Certificates include the ability to maintain a chain of parents. Each
220 # certificate includes a pointer to it's parent certificate. When loaded
221 # from a file or a string, the parent chain will be automatically loaded.
222 # When saving a certificate to a file or a string, the caller can choose
223 # whether to save the parent certificates as well.
234 separator="-----parent-----"
237 # Create a certificate object.
239 # @param create If create==True, then also create a blank X509 certificate.
240 # @param subject If subject!=None, then create a blank certificate and set
242 # @param string If string!=None, load the certficate from the string.
243 # @param filename If filename!=None, load the certficiate from the file.
245 def __init__(self, create=False, subject=None, string=None, filename=None):
246 if create or subject:
249 self.set_subject(subject)
251 self.load_from_string(string)
253 self.load_from_file(filename)
256 # Create a blank X509 certificate and store it in this object.
259 self.cert = crypto.X509()
260 self.cert.set_serial_number(1)
261 self.cert.gmtime_adj_notBefore(0)
262 self.cert.gmtime_adj_notAfter(60*60*24*365*5) # five years
265 # Given a pyOpenSSL X509 object, store that object inside of this
266 # certificate object.
268 def load_from_pyopenssl_x509(self, x509):
272 # Load the certificate from a string
274 def load_from_string(self, string):
275 # if it is a chain of multiple certs, then split off the first one and
277 parts = string.split(Certificate.separator, 1)
278 self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0])
280 # if there are more certs, then create a parent and let the parent load
281 # itself from the remainder of the string
283 self.parent = self.__class__()
284 self.parent.load_from_string(parts[1])
287 # Load the certificate from a file
289 def load_from_file(self, filename):
290 file = open(filename)
292 self.load_from_string(string)
295 # Save the certificate to a string.
297 # @param save_parents If save_parents==True, then also save the parent certificates.
299 def save_to_string(self, save_parents=False):
300 string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
301 if save_parents and self.parent:
302 string = string + Certificate.separator + self.parent.save_to_string(save_parents)
306 # Save the certificate to a file.
307 # @param save_parents If save_parents==True, then also save the parent certificates.
309 def save_to_file(self, filename, save_parents=False):
310 string = self.save_to_string(save_parents=save_parents)
311 open(filename, 'w').write(string)
314 # Sets the issuer private key and name
315 # @param key Keypair object containing the private key of the issuer
316 # @param subject String containing the name of the issuer
317 # @param cert (optional) Certificate object containing the name of the issuer
319 def set_issuer(self, key, subject=None, cert=None):
322 # it's a mistake to use subject and cert params at the same time
324 if isinstance(subject, dict) or isinstance(subject, str):
325 req = crypto.X509Req()
326 reqSubject = req.get_subject()
327 if (isinstance(subject, dict)):
328 for key in reqSubject.keys():
329 setattr(reqSubject, key, name[key])
331 setattr(reqSubject, "CN", subject)
333 # subject is not valid once req is out of scope, so save req
336 # if a cert was supplied, then get the subject from the cert
337 subject = cert.cert.get_issuer()
339 self.issuerSubject = subject
342 # Get the issuer name
344 def get_issuer(self, which="CN"):
345 x = self.cert.get_issuer()
346 return getattr(x, which)
349 # Set the subject name of the certificate
351 def set_subject(self, name):
352 req = crypto.X509Req()
353 subj = req.get_subject()
354 if (isinstance(name, dict)):
355 for key in name.keys():
356 setattr(subj, key, name[key])
358 setattr(subj, "CN", name)
359 self.cert.set_subject(subj)
361 # Get the subject name of the certificate
363 def get_subject(self, which="CN"):
364 x = self.cert.get_subject()
365 return getattr(x, which)
368 # Get the public key of the certificate.
370 # @param key Keypair object containing the public key
372 def set_pubkey(self, key):
373 assert(isinstance(key, Keypair))
374 self.cert.set_pubkey(key.get_openssl_pkey())
377 # Get the public key of the certificate.
378 # It is returned in the form of a Keypair object.
380 def get_pubkey(self):
381 m2x509 = X509.load_cert_string(self.save_to_string())
383 pkey.key = self.cert.get_pubkey()
384 pkey.m2key = m2x509.get_pubkey()
388 # Add an X509 extension to the certificate. Add_extension can only be called
389 # once for a particular extension name, due to limitations in the underlying
392 # @param name string containing name of extension
393 # @param value string containing value of the extension
395 def add_extension(self, name, critical, value):
396 ext = crypto.X509Extension (name, critical, value)
397 self.cert.add_extensions([ext])
400 # Get an X509 extension from the certificate
402 def get_extension(self, name):
403 # pyOpenSSL does not have a way to get extensions
404 m2x509 = X509.load_cert_string(self.save_to_string())
405 value = m2x509.get_ext(name).get_value()
409 # Set_data is a wrapper around add_extension. It stores the parameter str in
410 # the X509 subject_alt_name extension. Set_data can only be called once, due
411 # to limitations in the underlying library.
413 def set_data(self, str):
414 # pyOpenSSL only allows us to add extensions, so if we try to set the
415 # same extension more than once, it will not work
416 if self.data != None:
417 raise "cannot set subjectAltName more than once"
419 self.add_extension("subjectAltName", 0, "URI:http://" + str)
422 # Return the data string that was previously set with set_data
429 uri = self.get_extension("subjectAltName")
434 if not uri.startswith("URI:http://"):
435 raise "bad encoding in subjectAltName"
440 # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer().
443 assert self.cert != None
444 assert self.issuerSubject != None
445 assert self.issuerKey != None
446 self.cert.set_issuer(self.issuerSubject)
447 self.cert.sign(self.issuerKey.get_openssl_pkey(), self.digest)
450 # Verify the authenticity of a certificate.
451 # @param pkey is a Keypair object representing a public key. If Pkey
452 # did not sign the certificate, then an exception will be thrown.
454 def verify(self, pkey):
455 # pyOpenSSL does not have a way to verify signatures
456 m2x509 = X509.load_cert_string(self.save_to_string())
457 m2pkey = pkey.get_m2_pkey()
459 return m2x509.verify(m2pkey)
461 # XXX alternatively, if openssl has been patched, do the much simpler:
463 # self.cert.verify(pkey.get_openssl_key())
469 # Return True if pkey is identical to the public key that is contained in the certificate.
470 # @param pkey Keypair object
472 def is_pubkey(self, pkey):
473 return self.get_pubkey().is_same(pkey)
476 # Given a certificate cert, verify that this certificate was signed by the
477 # public key contained in cert. Throw an exception otherwise.
479 # @param cert certificate object
481 def is_signed_by_cert(self, cert):
482 k = cert.get_pubkey()
483 result = self.verify(k)
487 # Set the parent certficiate.
489 # @param p certificate object.
491 def set_parent(self, p):
495 # Return the certificate object of the parent of this certificate.
497 def get_parent(self):
501 # Verification examines a chain of certificates to ensure that each parent
502 # signs the child, and that some certificate in the chain is signed by a
503 # trusted certificate.
505 # Verification is a basic recursion: <pre>
506 # if this_certificate was signed by trusted_certs:
509 # return verify_chain(parent, trusted_certs)
512 # At each recursion, the parent is tested to ensure that it did sign the
513 # child. If a parent did not sign a child, then an exception is thrown. If
514 # the bottom of the recursion is reached and the certificate does not match
515 # a trusted root, then an exception is thrown.
517 # @param Trusted_certs is a list of certificates that are trusted.
520 def verify_chain(self, trusted_certs = None):
521 # Verify a chain of certificates. Each certificate must be signed by
522 # the public key contained in it's parent. The chain is recursed
523 # until a certificate is found that is signed by a trusted root.
525 # TODO: verify expiration time
527 # if this cert is signed by a trusted_cert, then we are set
528 for trusted_cert in trusted_certs:
529 # TODO: verify expiration of trusted_cert ?
530 if self.is_signed_by_cert(trusted_cert):
531 #print self.get_subject(), "is signed by a root"
534 # if there is no parent, then no way to verify the chain
536 #print self.get_subject(), "has no parent"
537 raise CertMissingParent(self.get_subject())
539 # if it wasn't signed by the parent...
540 if not self.is_signed_by_cert(self.parent):
541 #print self.get_subject(), "is not signed by parent"
542 return CertNotSignedByParent(self.get_subject())
544 # if the parent isn't verified...
545 self.parent.verify_chain(trusted_certs)