From 8a62587a6eab026c879a504074e013ac66c4b0dc Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sun, 30 Nov 2014 02:31:44 -0800 Subject: [PATCH] Added namedtuples for key, csr in client --- letsencrypt/client/apache_configurator.py | 47 +++++------ letsencrypt/client/client.py | 97 +++++++++++------------ letsencrypt/client/crypto_util.py | 2 +- letsencrypt/scripts/main.py | 21 ++--- 4 files changed, 81 insertions(+), 86 deletions(-) diff --git a/letsencrypt/client/apache_configurator.py b/letsencrypt/client/apache_configurator.py index adacebdc3..d7f62c125 100644 --- a/letsencrypt/client/apache_configurator.py +++ b/letsencrypt/client/apache_configurator.py @@ -196,7 +196,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): def choose_virtual_host(self, target_name): """ Chooses a virtual host based on the given domain name. - .. todo:: This should maybe return list if no obvious answer is presented + .. todo:: This should maybe return list if no obvious answer + is presented. :param str name: domain name @@ -286,7 +287,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): .. todo:: This will have to be updated for other distros versions - :param str filename: optional filename that will be used as the user config + :param str filename: optional filename that will be used as the + user config """ if filename: @@ -1253,12 +1255,12 @@ LogLevel warn \n\ """Peform a DVSNI challenge. Composed of - list_sni_tuple: List of tuples with form (addr, r, nonce) - addr (string), r (base64 string), nonce (hex string) - dvsni_key: string - File path to key + list_sni_tuple: List of tuples with form (addr, r, nonce) + addr (string), r (base64 string), nonce (hex string) - :param chall_dict: dvsni challenge - see documentation - :type chall_dict: dict + dvsni_key: namedtuple - client.Client.Key() + + :param dict chall_dict: dvsni challenge - see documentation """ # Save any changes to the configuration as a precaution @@ -1326,10 +1328,11 @@ LogLevel warn \n\ Result: Apache config includes virtual servers for issued challs :param list_sni_tuple: list of tuples with the form (addr, y, nonce) - addr (string), y (byte array), nonce (hex str) + addr (string), y (byte array), nonce (hex str) :type list_sni_tuple: list - :param str dvsni_key: file path to key + :param dvsni_key: Namedtuple with file, pem + :type dvsni_key: `client.Client.Key` - namedtuple :param list ll_addrs: list of list of addresses to apply @@ -1349,7 +1352,7 @@ LogLevel warn \n\ config_text = " \n" for idx, lis in enumerate(ll_addrs): config_text += get_config_text( - list_sni_tuple[idx][2], lis, dvsni_key) + list_sni_tuple[idx][2], lis, dvsni_key.file) config_text += " \n" self.dvsni_conf_include_check(self.user_config_file) @@ -1373,26 +1376,21 @@ LogLevel warn \n\ self.add_dir("/files" + main_config, "Include", CONFIG.APACHE_CHALLENGE_CONF) - def dvsni_create_chall_cert(self, name, ext, nonce, key_file): + def dvsni_create_chall_cert(self, name, ext, nonce, dvsni_key): """Creates DVSNI challenge certifiate. Certificate created at dvsni_get_cert_file(nonce) :param str nonce: hex form of nonce - :param str key_file: absolute path to key file + + :param dvsni_key: absolute path to key file + :type dvsni_key: `client.Client.Key` """ - try: - with open(key_file, 'r') as key_fd: - key_str = key_fd.read() - except IOError: - raise errors.LetsEncryptDvsniError( - "Unable to load key file: %s" % key_file) - self.register_file_creation(True, dvsni_get_cert_file(nonce)) cert_pem = crypto_util.make_ss_cert( - key_str, [nonce + CONFIG.INVALID_EXT, name, ext]) + dvsni_key.pem, [nonce + CONFIG.INVALID_EXT, name, ext]) with open(dvsni_get_cert_file(nonce), 'w') as chall_cert_file: chall_cert_file.write(cert_pem) @@ -1513,7 +1511,7 @@ def strip_dir(path): .. todo:: Replace this with Python standard function :param str path: path is a file path. not an augeas section or - directive path + directive path :returns: directory :rtype: str @@ -1539,7 +1537,7 @@ def dvsni_get_cert_file(nonce): return CONFIG.WORK_DIR + nonce + ".crt" -def get_config_text(nonce, ip_addrs, key): +def get_config_text(nonce, ip_addrs, dvsni_key_file): """Chocolate virtual server configuration text :param nonce: hex form of nonce @@ -1548,8 +1546,7 @@ def get_config_text(nonce, ip_addrs, key): :param ip_addrs: addresses of challenged domain :type ip_addrs: str - :param key: file path to key - :type key: str + :param str dvsni_key_file: Path to key file :returns: virtual host configuration text :rtype: str @@ -1564,7 +1561,7 @@ def get_config_text(nonce, ip_addrs, key): "\n" "Include " + CONFIG.OPTIONS_SSL_CONF + " \n" "SSLCertificateFile " + dvsni_get_cert_file(nonce) + " \n" - "SSLCertificateKeyFile " + key + " \n" + "SSLCertificateKeyFile " + dvsni_key_file + " \n" "\n" "DocumentRoot " + CONFIG.CONFIG_DIR + "challenge_page/ \n" " \n\n") diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index a95698912..653b4b6c6 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,4 +1,5 @@ """ACME protocol client class and helper functions.""" +import collections import csv import json import os @@ -31,15 +32,16 @@ ALLOW_RAW_IPV6_SERVER = False class Client(object): """ACME protocol client.""" + Key = collections.namedtuple("Key", "file pem") + CSR = collections.namedtuple("CSR", "file data type") - def __init__(self, ca_server, cert_signing_request=None, - private_key=None, private_key_file=None, use_curses=True): + def __init__(self, ca_server, cert_signing_request=CSR(None, None, None), + private_key=Key(None, None), use_curses=True): """ :param str ca_server: Certificate authority server :param str cert_signing_request: Contents of the CSR :param str private_key: Contents of the private key - :param str private_key_file: absolute path to private_key :param bool use_curses: Use curses UI """ @@ -53,9 +55,10 @@ class Client(object): self.config = apache_configurator.ApacheConfigurator( CONFIG.SERVER_ROOT) self.server = ca_server + + # These are CSR/Key namedtuples self.csr = cert_signing_request self.privkey = private_key - self.privkey_file = private_key_file # TODO: Figure out all exceptions from this function try: @@ -115,11 +118,11 @@ class Client(object): # Request Challenges challenge_msg = self.acme_challenge() - # Get key and csr to perform challenges - _, csr_der = self.get_key_csr_pem() + # Make sure we have key and csr to perform challenges + self.init_key_csr() # TODO: Handle this exception/problem - if not crypto_util.csr_matches_names(self.csr, self.names): + if not crypto_util.csr_matches_names(self.csr.data, self.names): raise errors.LetsEncryptClientError( "CSR subject does not contain one of the specified names") @@ -129,7 +132,7 @@ class Client(object): self.acme_authorization(challenge_msg, challenge_objs, responses) # Retrieve certificate - certificate_dict = self.acme_certificate(csr_der) + certificate_dict = self.acme_certificate(self.csr.data) # Find set of virtual hosts to deploy certificates to vhost = self.get_virtual_hosts(self.names) @@ -170,7 +173,7 @@ class Client(object): """ auth_dict = self.send(acme.authorization_request( challenge_msg["sessionID"], self.names[0], - challenge_msg["nonce"], responses, self.privkey)) + challenge_msg["nonce"], responses, self.privkey.pem)) try: return self.is_expected_msg(auth_dict, "authorization") @@ -192,7 +195,7 @@ class Client(object): """ logger.info("Preparing and sending CSR..") return self.send_and_receive_expected( - acme.certificate_request(csr_der, self.privkey), "certificate") + acme.certificate_request(csr_der, self.privkey.pem), "certificate") def acme_revocation(self, cert): """Handle ACME "revocation" phase. @@ -411,7 +414,7 @@ class Client(object): for host in vhost: self.config.deploy_cert(host, os.path.abspath(cert_file), - os.path.abspath(self.privkey_file), + os.path.abspath(self.privkey.file), cert_chain_abspath) # Enable any vhost that was issued to, but not enabled if not host.enabled: @@ -519,17 +522,17 @@ class Client(object): for row in csvreader: idx = int(row[0]) + 1 csvwriter = csv.writer(csvfile) - csvwriter.writerow([str(idx), cert_file, self.privkey_file]) + csvwriter.writerow([str(idx), cert_file, self.privkey.file]) else: with open(list_file, 'wb') as csvfile: csvwriter = csv.writer(csvfile) - csvwriter.writerow(["0", cert_file, self.privkey_file]) + csvwriter.writerow(["0", cert_file, self.privkey.file]) - shutil.copy2(self.privkey_file, + shutil.copy2(self.privkey.file, os.path.join( CONFIG.CERT_KEY_BACKUP, - os.path.basename(self.privkey_file) + "_" + str(idx))) + os.path.basename(self.privkey.file) + "_" + str(idx))) shutil.copy2(cert_file, os.path.join( CONFIG.CERT_KEY_BACKUP, @@ -602,34 +605,24 @@ class Client(object): challenge_objs.append({ "type": "dvsni", "list_sni_tuple": sni_todo, - "dvsni_key": os.path.abspath(self.privkey_file), + "dvsni_key": self.privkey, }) challenge_obj_indices.append(sni_satisfies) logger.debug(sni_todo) return challenge_objs, challenge_obj_indices - def get_key_csr_pem(self, csr_return_format='der'): - """Return key and CSR, generate if necessary. + def init_key_csr(self): + """Initializes privkey and csr. - Returns key and CSR using provided files or generating new files + Inits key and CSR using provided files or generating new files if necessary. Both will be saved in PEM format on the - filesystem. The CSR can optionally be returned in DER format as - the CSR cannot be loaded back into M2Crypto. - - :param csr_return_format: If "der" returned CSR is in DER format, - PEM otherwise. - :param csr_return_format: str - - :returns: A pair of `(key, csr)`, where `key` is PEM encoded `str` - and `csr` is PEM/DER (depedning on `csr_return_format` - encoded `str`. - :rtype: tuple + filesystem. The CSR is placed into DER format to allow + the namedtuple to easily work with the protocol. """ - if not self.privkey: + if not self.privkey.file: key_pem = crypto_util.make_key(CONFIG.RSA_KEY_SIZE) - self.privkey = key_pem # Save file le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700) @@ -638,14 +631,13 @@ class Client(object): key_f.write(key_pem) key_f.close() - self.privkey_file = key_filename - logger.info("Generating key: %s" % self.privkey_file) - else: - key_pem = self.privkey + logger.info("Generating key: %s" % key_filename) - if not self.csr: - csr_pem, csr_der = crypto_util.make_csr(self.privkey, self.names) - self.csr = csr_pem + self.privkey = Client.Key(key_filename, key_pem) + + if not self.csr.file: + csr_pem, csr_der = crypto_util.make_csr( + self.privkey.pem, self.names) # Save CSR le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755) @@ -653,15 +645,16 @@ class Client(object): os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644) csr_f.write(csr_pem) csr_f.close() - logger.info("Creating CSR: %s" % csr_filename) - else: - csr_obj = M2Crypto.X509.load_request_string(self.csr) - csr_pem, csr_der = csr_obj.as_pem(), csr_obj.as_der() - if csr_return_format == 'der': - return key_pem, csr_der - else: - return key_pem, csr_pem + logger.info("Creating CSR: %s" % csr_filename) + + self.csr = Client.CSR(csr_filename, csr_der, "der") + elif self.csr.type != "der": + # The user is going to pass in a pem format file + # That is why we must conver it to der since the + # protocol uses der exclusively. + csr_obj = M2Crypto.X509.load_request_string(self.csr.data) + self.csr = Client.CSR(self.csr.file, csr_obj.as_der(), "der") def _validate_csr_key_cli(self): """Validate CSR and key files. @@ -677,19 +670,21 @@ class Client(object): # and allow the user to take more appropriate actions # If CSR is provided, it must be readable and valid. - if self.csr and not crypto_util.valid_csr(self.csr): + if self.csr.data and not crypto_util.valid_csr(self.csr.data): raise errors.LetsEncryptClientError( "The provided CSR is not a valid CSR") # If key is provided, it must be readable and valid. - if self.privkey and not crypto_util.valid_privkey(self.privkey): + if (self.privkey.pem and + not crypto_util.valid_privkey(self.privkey.pem)): raise errors.LetsEncryptClientError( "The provided key is not a valid key") # If CSR and key are provided, the key must be the same key used # in the CSR. - if self.csr and self.privkey: - if not crypto_util.csr_matches_pubkey(self.csr, self.privkey): + if self.csr.data and self.privkey.pem: + if not crypto_util.csr_matches_pubkey( + self.csr.data, self.privkey.pem): raise errors.LetsEncryptClientError( "The key and CSR do not match") diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index a53edc85a..318dcd415 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -238,7 +238,7 @@ def csr_matches_names(csr, domains): """ try: - csr_obj = M2Crypto.X509.load_request_string(csr) + csr_obj = M2Crypto.X509.load_request_der_string(csr) return csr_obj.get_subject().CN in domains except M2Crypto.X509.X509Error: return False diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index fdd5746b1..78afabe4f 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -28,10 +28,10 @@ def main(): nargs="+") parser.add_argument("-s", "--server", dest="server", help="The ACME CA server address.") - parser.add_argument("-p", "--privkey", dest="privkey_tup", type=read_file, + parser.add_argument("-p", "--privkey", dest="privkey", type=read_file, help="Path to the private key file for certificate " "generation.") - parser.add_argument("-c", "--csr", dest="csr_tup", type=read_file, + parser.add_argument("-c", "--csr", dest="csr", type=read_file, help="Path to the certificate signing request file " "corresponding to the private key file. The " "private key file argument is required if this " @@ -63,7 +63,7 @@ def main(): args = parser.parse_args() # Enforce '--privkey' is set along with '--csr'. - if args.csr_tup and not args.privkey_tup: + if args.csr and not args.privkey: parser.error("private key file (--privkey) must be specified along{0} " "with the certificate signing request file (--csr)" .format(os.linesep)) @@ -84,13 +84,16 @@ def main(): server = args.server is None and CONFIG.ACME_SERVER or args.server # Prepare for init of Client - if args.privkey_tup is None: - args.privkey_tup = (None, None) - if args.csr_tup is None: - args.csr_tup = (None, None) + if args.privkey is None: + privkey = client.Client.Key(None, None) + else: + privkey = client.Client.Key(args.privkey[0], args.privkey[1]) + if args.csr is None: + csr = client.Client.CSR(None, None, None) + else: + csr = client.Client.CSR(args.csr[0], args.csr[1], "pem") - acme = client.Client(server, args.csr_tup[1], args.privkey_tup[1], - args.privkey_tup[0], args.curses) + acme = client.Client(server, csr, privkey, args.curses) if args.revoke: acme.list_certs_keys() else: