From da29a927293f845bb836370831ac3a75387f90e8 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Thu, 27 Nov 2014 08:12:40 -0500 Subject: [PATCH 01/17] Better error checking on private key and csr. Also, pull in file names instead of pointers. --- letsencrypt/scripts/main.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 02c9e57e0..c6f930356 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", type=file, + parser.add_argument("-p", "--privkey", dest="privkey", type=str, help="Path to the private key file for certificate " "generation.") - parser.add_argument("-c", "--csr", dest="csr", type=file, + parser.add_argument("-c", "--csr", dest="csr", type=str, help="Path to the certificate signing request file " "corresponding to the private key file. The " "private key file argument is required if this " @@ -63,10 +63,14 @@ def main(): args = parser.parse_args() - # Enforce --privkey is set along with --csr. + # Make sure each given file is readable. + for f in (args.privkey, args.csr): + if f and not os.access(f, os.R_OK): + parser.error("the file '{}' is not readable.".format(f)) + + # Enforce '--privkey' is set along with '--csr'. if args.csr and not args.privkey: - parser.print_usage() - parser.error("private key file (--privkey) must be specified along{}" + parser.error("private key file (--privkey) must be specified along{} " "with the certificate signing request file (--csr)" .format(os.linesep)) From bcb788fe0bf4cb9f67756b3a6e4bc4301b64ef70 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Thu, 27 Nov 2014 08:39:32 -0500 Subject: [PATCH 02/17] Cleaned up per Kuba. --- letsencrypt/scripts/main.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index c6f930356..aa8bd82a5 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", type=str, + parser.add_argument("-p", "--privkey", dest="privkey", help="Path to the private key file for certificate " "generation.") - parser.add_argument("-c", "--csr", dest="csr", type=str, + parser.add_argument("-c", "--csr", dest="csr", help="Path to the certificate signing request file " "corresponding to the private key file. The " "private key file argument is required if this " @@ -63,11 +63,6 @@ def main(): args = parser.parse_args() - # Make sure each given file is readable. - for f in (args.privkey, args.csr): - if f and not os.access(f, os.R_OK): - parser.error("the file '{}' is not readable.".format(f)) - # Enforce '--privkey' is set along with '--csr'. if args.csr and not args.privkey: parser.error("private key file (--privkey) must be specified along{} " From 6d9edda822ad165c6ce5e2d43efc6d97b9068cc9 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Thu, 27 Nov 2014 08:49:21 -0500 Subject: [PATCH 03/17] Restore 'type=file' on privkey and csr command line arguments. --- letsencrypt/scripts/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index aa8bd82a5..b048f0212 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", + parser.add_argument("-p", "--privkey", dest="privkey", type=file, help="Path to the private key file for certificate " "generation.") - parser.add_argument("-c", "--csr", dest="csr", + parser.add_argument("-c", "--csr", dest="csr", type=file, help="Path to the certificate signing request file " "corresponding to the private key file. The " "private key file argument is required if this " From ddca02cb7b55c680786e36c33ef8e9a5602338ff Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Thu, 27 Nov 2014 09:13:01 -0500 Subject: [PATCH 04/17] Fixed format string incompatibility with py2.6. --- letsencrypt/scripts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index b048f0212..8bac91090 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -65,7 +65,7 @@ def main(): # Enforce '--privkey' is set along with '--csr'. if args.csr and not args.privkey: - parser.error("private key file (--privkey) must be specified along{} " + parser.error("private key file (--privkey) must be specified along{0} " "with the certificate signing request file (--csr)" .format(os.linesep)) From 6254038ea3d7cec4dd86a765a51add0c489e9d28 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Fri, 28 Nov 2014 11:41:03 -0500 Subject: [PATCH 05/17] Provide more user-friendly errors when opening file handles. --- letsencrypt/scripts/main.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 8bac91090..8a548a5fc 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", type=file, + parser.add_argument("-p", "--privkey", dest="privkey", type=open_file, help="Path to the private key file for certificate " "generation.") - parser.add_argument("-c", "--csr", dest="csr", type=file, + parser.add_argument("-c", "--csr", dest="csr", type=open_file, help="Path to the certificate signing request file " "corresponding to the private key file. The " "private key file argument is required if this " @@ -61,7 +61,10 @@ def main(): parser.add_argument("--test", dest="test", action="store_true", help="Run in test mode.") - args = parser.parse_args() + try: + args = parser.parse_args() + except IOError as e: + parser.error(e) # Enforce '--privkey' is set along with '--csr'. if args.csr and not args.privkey: @@ -91,6 +94,26 @@ def main(): acme.authenticate(args.domains, args.redirect, args.eula) +def open_file(filename): + """Returns a file object for the given filename. + + :param filename: Filename + :type filename: str + + :return: file object + :raise IOError: File does not exist or is not readable. + + """ + + if not os.path.exists(filename): + raise IOError("the file '{0}' is not found".format(filename)) + + if not os.access(filename, os.R_OK): + raise IOError("the file '{0}' is not readable".format(filename)) + + return file(filename) + + def rollback(config, checkpoints): """Revert configuration the specified number of checkpoints. From 122e6b2ca1c1c6283f565f1f0ba7064800eea032 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Fri, 28 Nov 2014 13:28:16 -0500 Subject: [PATCH 06/17] Read in files with universal newline support. --- letsencrypt/scripts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 8a548a5fc..06a277283 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -111,7 +111,7 @@ def open_file(filename): if not os.access(filename, os.R_OK): raise IOError("the file '{0}' is not readable".format(filename)) - return file(filename) + return file(filename, 'rU') def rollback(config, checkpoints): From 8464ce30d55dedcd203f25ab0e7e7c11487b3b4d Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Fri, 28 Nov 2014 15:47:06 -0500 Subject: [PATCH 07/17] More sensible mode defaults when creating a unique file. --- letsencrypt/client/le_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/client/le_util.py b/letsencrypt/client/le_util.py index 19070858f..e1758c02a 100644 --- a/letsencrypt/client/le_util.py +++ b/letsencrypt/client/le_util.py @@ -50,7 +50,7 @@ def check_permissions(filepath, mode, uid=0): return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid -def unique_file(default_name, mode=0777): +def unique_file(default_name, mode=0644): """Safely finds a unique file for writing only (by default).""" count = 1 f_parsed = os.path.splitext(default_name) From 3ecf8659a1e755282ce6eecda22a65d125792272 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Fri, 28 Nov 2014 15:48:04 -0500 Subject: [PATCH 08/17] Work with CSR and private key file contents in the client. --- letsencrypt/client/client.py | 64 ++++++++++++++++++------------- letsencrypt/client/crypto_util.py | 52 +++++++++---------------- letsencrypt/scripts/main.py | 21 +++++----- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 3fdcd7c1f..6af13a9b4 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -44,13 +44,13 @@ class Client(object): CONFIG.SERVER_ROOT) self.server = ca_server - + if cert_signing_request: - self.csr_file = cert_signing_request.name + self.csr_file = cert_signing_request else: self.csr_file = None if private_key: - self.key_file = private_key.name + self.key_file = private_key else: self.key_file = None @@ -65,6 +65,16 @@ class Client(object): self.server_url = "https://%s/acme/" % self.server def authenticate(self, domains=None, redirect=None, eula=False): + """ + + :param domains: List of domains + :type domains: list + :param redirect: + :type redirect: bool|None + :param eula: EULA accepted + :type eula: bool + :raise Exception: CSR does not contain one of the specified names. + """ domains = [] if domains is None else domains # Check configuration @@ -248,7 +258,7 @@ class Client(object): """Send ACME message to server and return expected message. :param msg: ACME message (JSON serializable). - :type acem_msg: dict + :type msg: dict :param expected: Name of the expected response ACME message type. :type expected: str @@ -552,7 +562,7 @@ class Client(object): :param name: TODO :type name: TODO - :param challanges: A list of challenges from ACME "challenge" + :param challenges: A list of challenges from ACME "challenge" server message to be fulfilled by the client in order to prove possession of the identifier. :type challenges: list @@ -673,33 +683,19 @@ class Client(object): # The client can eventually do things like prompt the user # and allow the user to take more appropriate actions - # If CSR is provided, the private key should also be provided. - if self.csr_file and not self.key_file: - logger.fatal(("Please provide the private key file used in " - "generating the provided CSR")) - sys.exit(1) # If CSR is provided, it must be readable and valid. - try: - if self.csr_file and not crypto_util.valid_csr(self.csr_file): - raise Exception("The provided CSR is not a valid CSR") - except IOError: - raise Exception("The provided CSR could not be read") + if self.csr_file and not crypto_util.valid_csr(self.csr_file): + raise Exception("The provided CSR is not a valid CSR") + # If key is provided, it must be readable and valid. - try: - if self.key_file and not crypto_util.valid_privkey(self.key_file): - raise Exception("The provided key is not a valid key") - except IOError: - raise Exception("The provided key could not be read") + if self.key_file and not crypto_util.valid_privkey(self.key_file): + raise Exception("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_file and self.key_file: - try: - if not crypto_util.csr_matches_pubkey( - self.csr_file, self.key_file): - raise Exception("The key and CSR do not match") - except IOError: - raise Exception("The key or CSR files could not be read") + if not crypto_util.csr_matches_pubkey(self.csr_file, self.key_file): + raise Exception("The key and CSR do not match") def get_all_names(self): """Return all valid names in the configuration.""" @@ -753,6 +749,12 @@ def remove_cert_key(cert): def sanity_check_names(names): + """Make sure host names are valid. + + :param names: List of host names + :type names: list + + """ for name in names: if not is_hostname_sane(name): logger.fatal(repr(name) + " is an impossible hostname") @@ -760,9 +762,17 @@ def sanity_check_names(names): def is_hostname_sane(hostname): - """ + """Make sure the given host name is sane. + Do enough to avoid shellcode from the environment. There's no need to do more. + + :param hostname: Host name to validate + :type hostname: str + + :returns: True if hostname is valid, otherwise false. + :rtype: bool + """ # hostnames & IPv4 allowed = string.ascii_letters + string.digits + "-." diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 2dd291d9c..e09368cbe 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -206,39 +206,33 @@ def get_cert_info(filename): # A. Do more checks to verify that the CSR is trusted/valid # B. Audit the parsing code for vulnerabilities -def valid_csr(csr_filename): +def valid_csr(csr_file): """Validate CSR. Check if `csr_filename` is a valid CSR for the given domains. - TODO: Currently, could raise non-X.509-related errors such as IOError - associated with problems reading the file. Comment or handle. - - :param csr_filename: Path to the purported CSR file. - :type csr_filename: str + :param csr_file: CSR file contents + :type csr_file: str :returns: Validity of CSR. :rtype: bool """ try: - csr = M2Crypto.X509.load_request(csr_filename) + csr = M2Crypto.X509.load_request_string(csr_file) return bool(csr.verify(csr.get_pubkey())) except M2Crypto.X509.X509Error: return False -def csr_matches_names(csr_filename, domains): +def csr_matches_names(csr_file, domains): """Check if CSR contains the subject of one of the domains. M2Crypto currently does not expose the OpenSSL interface to also check the SAN extension. This is insufficient for full testing - TODO: Currently, could raise non-X.509-related errors such as IOError - associated with problems reading the file. Comment or handle. - - :param csr_filename: Path to the purported CSR file. - :type csr_filename: str + :param csr_file: CSR file contents + :type csr_file: str :param domains: Domains the CSR should contain. :type domains: list @@ -248,49 +242,41 @@ def csr_matches_names(csr_filename, domains): """ try: - csr = M2Crypto.X509.load_request(csr_filename) + csr = M2Crypto.X509.load_request_string(csr_file) return csr.get_subject().CN in domains except M2Crypto.X509.X509Error: return False -def valid_privkey(privkey_filename): +def valid_privkey(privkey_file): """Is valid RSA private key? - TODO: Currently, could raise non-X.509-related errors such as IOError - associated with problems reading the file. Comment or handle. - - :param privkey_filename: Path to the purported private key file. - :type privkey_filename: str + :param privkey_file: Private key file contents + :type privkey_file: str :returns: Validity of private key. :rtype: bool """ try: - return bool(M2Crypto.RSA.load_key(privkey_filename).check_key()) + return bool(M2Crypto.RSA.load_key_string(privkey_file).check_key()) except M2Crypto.RSA.RSAError: return False -def csr_matches_pubkey(csr_filename, privkey_filename): +def csr_matches_pubkey(csr_file, privkey_file): """Does private key correspond to the subject public key in the CSR? - TODO: Currently, could raise non-X.509-related errors such as IOError - associated with problems reading the file. Comment or handle. + :param csr_file: CSR file contents + :type csr_file: str - TODO: Seems that this doesn not handle X509 eceptions either. - - :param csr_filename: Path to the purported CSR file. - :type csr_filename: str - - :param privkey_filename: Path to the purported private key file. - :type privkey_filename: str + :param privkey_file: Private key file contents + :type privkey_file: str :returns: Correspondence of private key to CSR subject public key. :rtype: bool """ - csr = M2Crypto.X509.load_request(csr_filename) - privkey = M2Crypto.RSA.load_key(privkey_filename) + csr = M2Crypto.X509.load_request_string(csr_file) + privkey = M2Crypto.RSA.load_key_string(privkey_file) return csr.get_pubkey().get_rsa().pub() == privkey.pub() diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 06a277283..22a5cd824 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", type=open_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", type=open_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,8 +63,8 @@ def main(): try: args = parser.parse_args() - except IOError as e: - parser.error(e) + except IOError as exc: + parser.error(exc) # Enforce '--privkey' is set along with '--csr'. if args.csr and not args.privkey: @@ -94,14 +94,17 @@ def main(): acme.authenticate(args.domains, args.redirect, args.eula) -def open_file(filename): - """Returns a file object for the given filename. +def read_file(filename): + """Returns the given file's contents with universal new line support. :param filename: Filename :type filename: str - :return: file object - :raise IOError: File does not exist or is not readable. + :returns: File contents + :rtype: str + :raise IOError: File does not exist or is not readable. file() will + potentially throw its own IOError exceptions in addition to the two + explicitely thrown. """ @@ -111,7 +114,7 @@ def open_file(filename): if not os.access(filename, os.R_OK): raise IOError("the file '{0}' is not readable".format(filename)) - return file(filename, 'rU') + return file(filename, 'rU').read() def rollback(config, checkpoints): From 87d7ed175070d3d4ab17dc8a50b6303d20dbdb0f Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Fri, 28 Nov 2014 16:47:37 -0500 Subject: [PATCH 09/17] Clarified --rollback command line option. --- README.md | 3 +-- letsencrypt/scripts/main.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 878652b41..4af884374 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,7 @@ optional arguments: corresponding to the private key file. The private key file argument is required if this argument is specified. - -b ROLLBACK, --rollback ROLLBACK - Revert configuration number of checkpoints. + -b N, --rollback N Revert configuration N number of checkpoints. -k, --revoke Revoke a certificate. -v, --view-checkpoints View checkpoints and associated configuration changes. diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 22a5cd824..d379c5692 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -37,9 +37,8 @@ def main(): "private key file argument is required if this " "argument is specified.") parser.add_argument("-b", "--rollback", dest="rollback", type=int, - default=0, - help="Revert configuration number of " - "checkpoints.") + default=0, metavar="N", + help="Revert configuration N number of checkpoints.") parser.add_argument("-k", "--revoke", dest="revoke", action="store_true", help="Revoke a certificate.") parser.add_argument("-v", "--view-checkpoints", dest="view_checkpoints", From c0c731b3e6509b7d80c82fb8bdb1f0d82540d523 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Fri, 28 Nov 2014 17:00:02 -0500 Subject: [PATCH 10/17] Updated get_key_csr_pem() to use file contents instead of file names. --- letsencrypt/client/client.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 6af13a9b4..51682131f 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -635,37 +635,28 @@ class Client(object): key_pem = None csr_pem = None if not self.key_file: - key_pem = crypto_util.make_key(CONFIG.RSA_KEY_SIZE) + self.key_file = crypto_util.make_key(CONFIG.RSA_KEY_SIZE) # Save file le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0700) - key_f, self.key_file = le_util.unique_file( + key_f, key_filename = le_util.unique_file( os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0600) - key_f.write(key_pem) + key_f.write(self.key_file) key_f.close() - logger.info("Generating key: %s" % self.key_file) - else: - try: - key_pem = open(self.key_file).read().replace("\r", "") - except: - logger.fatal("Unable to open key file: %s" % self.key_file) - sys.exit(1) + logger.info("Generating key: %s" % key_filename) if not self.csr_file: - csr_pem, csr_der = crypto_util.make_csr(self.key_file, self.names) + self.csr_file, csr_der = crypto_util.make_csr(self.key_file, + self.names) # Save CSR le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0755) - csr_f, self.csr_file = le_util.unique_file( + csr_f, csr_filename = le_util.unique_file( os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0644) - csr_f.write(csr_pem) + csr_f.write(self.csr_file) csr_f.close() - logger.info("Creating CSR: %s" % self.csr_file) + logger.info("Creating CSR: %s" % csr_filename) else: - try: - csr = M2Crypto.X509.load_request(self.csr_file) - csr_pem, csr_der = csr.as_pem(), csr.as_der() - except: - logger.fatal("Unable to open CSR file: %s" % self.csr_file) - sys.exit(1) + csr = M2Crypto.X509.load_request_string(self.csr_file) + csr_pem, csr_der = csr.as_pem(), csr.as_der() if csr_return_format == 'der': return key_pem, csr_der From 5854d426721cbcaaeff9e911d83795a9f5d9a2b6 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Sat, 29 Nov 2014 11:15:56 -0500 Subject: [PATCH 11/17] Various bug, docstring, and PEP8 fixes. --- letsencrypt/client/client.py | 125 ++++++++++++++++++------------ letsencrypt/client/crypto_util.py | 47 +++++------ letsencrypt/scripts/main.py | 22 ++---- 3 files changed, 107 insertions(+), 87 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 51682131f..f501c10d0 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -1,3 +1,4 @@ +"""ACME protocol client class and helper functions.""" import csv import json import os @@ -33,6 +34,21 @@ class Client(object): def __init__(self, ca_server, cert_signing_request=None, private_key=None, use_curses=True): + """ + + :param ca_server: Certificate authority server + :type ca_server: str + + :param cert_signing_request: Contents of the CSR + :type cert_signing_request: str + + :param private_key: Contents of the private key + :type private_key: str + + :param use_curses: Use curses UI + :type use_curses: bool + + """ self.curses = use_curses # Logger needs to be initialized before Configurator @@ -42,25 +58,17 @@ class Client(object): # line arg or client function to discover self.config = apache_configurator.ApacheConfigurator( CONFIG.SERVER_ROOT) - self.server = ca_server - - if cert_signing_request: - self.csr_file = cert_signing_request - else: - self.csr_file = None - if private_key: - self.key_file = private_key - else: - self.key_file = None + self.csr = cert_signing_request + self.privkey = private_key # TODO: Figure out all exceptions from this function try: self._validate_csr_key_cli() - except Exception as e: + except Exception as exc: # TODO: Something nice here... logger.fatal(("%s - until the programmers get their act together, " - "we are just going to exit" % str(e))) + "we are just going to exit" % str(exc))) sys.exit(1) self.server_url = "https://%s/acme/" % self.server @@ -69,11 +77,16 @@ class Client(object): :param domains: List of domains :type domains: list + :param redirect: :type redirect: bool|None + :param eula: EULA accepted :type eula: bool - :raise Exception: CSR does not contain one of the specified names. + + :raises errors.LetsEncryptClientError: CSR does not contain one of the + specified names. + """ domains = [] if domains is None else domains @@ -112,9 +125,9 @@ class Client(object): _, csr_der = self.get_key_csr_pem() # TODO: Handle this exception/problem - if not crypto_util.csr_matches_names(self.csr_file, self.names): - raise Exception(("CSR subject does not contain one of the " - "specified names")) + if not crypto_util.csr_matches_names(self.csr, self.names): + raise errors.LetsEncryptClientError(("CSR subject does not contain " + "one of the specified names")) # Perform Challenges responses, challenge_objs = self.verify_identity(challenge_msg) @@ -165,7 +178,7 @@ class Client(object): """ auth_dict = self.send(acme.authorization_request( challenge_msg["sessionID"], self.names[0], - challenge_msg["nonce"], responses, self.key_file)) + challenge_msg["nonce"], responses, self.privkey)) try: return self.is_expected_msg(auth_dict, "authorization") @@ -188,7 +201,7 @@ class Client(object): """ logger.info("Preparing and sending CSR..") return self.send_and_receive_expected( - acme.certificate_request(csr_der, self.key_file), "certificate") + acme.certificate_request(csr_der, self.privkey), "certificate") def acme_revocation(self, cert): """Handle ACME "revocation" phase. @@ -221,14 +234,14 @@ class Client(object): :param msg: ACME message (JSON serializable). :type msg: dict - :raises: TypeError if `msg` is not JSON serializable or - jsonschema.ValidationError if not valid ACME message or - `errors.LetsEncryptClientError` in case of connection error - or if response from server is not a valid ACME message. - :returns: Server response message. :rtype: dict + :raises TypeError: if `msg` is not JSON serializable + :raises jsonschema.ValidationError: if not valid ACME message + :raises errors.LetsEncryptClientError: in case of connection error + or if response from server is not a valid ACME message. + """ json_encoded = json.dumps(msg) acme.acme_object_validate(json_encoded) @@ -266,6 +279,8 @@ class Client(object): :returns: ACME response message of expected type. :rtype: dict + :raises errors.LetsEncryptClientError: An exception is thrown + """ response = self.send(msg) try: @@ -291,11 +306,11 @@ class Client(object): reponse message. :type rounds: int - :raises: Exception - :returns: ACME response message from server. :rtype: dict + :raises errors.LetsEncryptClientError: + """ for _ in xrange(rounds): if response["type"] == expected: @@ -322,6 +337,7 @@ class Client(object): (rounds * delay)) def list_certs_keys(self): + """List trusted Let's Encrypt certificates.""" list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") certs = [] @@ -415,7 +431,7 @@ class Client(object): for host in vhost: self.config.deploy_cert(host, os.path.abspath(cert_file), - os.path.abspath(self.key_file), + os.path.abspath(self.privkey), cert_chain_abspath) # Enable any vhost that was issued to, but not enabled if not host.enabled: @@ -506,6 +522,9 @@ class Client(object): :param encrypt: Should the certificate key be encrypted? :type encrypt: bool + :returns: True if key file was stored successfully, False otherwise. + :rtype: bool + """ list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0700) @@ -523,22 +542,24 @@ class Client(object): for row in csvreader: idx = int(row[0]) + 1 csvwriter = csv.writer(csvfile) - csvwriter.writerow([str(idx), cert_file, self.key_file]) + csvwriter.writerow([str(idx), cert_file, self.privkey]) else: with open(list_file, 'wb') as csvfile: csvwriter = csv.writer(csvfile) - csvwriter.writerow(["0", cert_file, self.key_file]) + csvwriter.writerow(["0", cert_file, self.privkey]) - shutil.copy2(self.key_file, + shutil.copy2(self.privkey, os.path.join( CONFIG.CERT_KEY_BACKUP, - os.path.basename(self.key_file) + "_" + str(idx))) + os.path.basename(self.privkey) + "_" + str(idx))) shutil.copy2(cert_file, os.path.join( CONFIG.CERT_KEY_BACKUP, os.path.basename(cert_file) + "_" + str(idx))) + return True + def redirect_to_ssl(self, vhost): for ssl_vh in vhost: success, redirect_vhost = self.config.enable_redirect(ssl_vh) @@ -607,7 +628,7 @@ class Client(object): challenge_objs.append({ "type": "dvsni", "listSNITuple": sni_todo, - "dvsni_key": os.path.abspath(self.key_file), + "dvsni_key": os.path.abspath(self.privkey), }) challenge_obj_indices.append(sni_satisfies) logger.debug(sni_todo) @@ -632,31 +653,34 @@ class Client(object): :rtype: tuple """ - key_pem = None - csr_pem = None - if not self.key_file: - self.key_file = crypto_util.make_key(CONFIG.RSA_KEY_SIZE) + if not self.privkey: + 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, 0700) key_f, key_filename = le_util.unique_file( os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0600) - key_f.write(self.key_file) + key_f.write(self.privkey) key_f.close() logger.info("Generating key: %s" % key_filename) + else: + key_pem = self.privkey + + if not self.csr: + csr_pem, csr_der = crypto_util.make_csr(self.privkey, self.names) + self.csr = csr_pem - if not self.csr_file: - self.csr_file, csr_der = crypto_util.make_csr(self.key_file, - self.names) # Save CSR le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0755) csr_f, csr_filename = le_util.unique_file( os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0644) - csr_f.write(self.csr_file) + csr_f.write(self.csr) csr_f.close() logger.info("Creating CSR: %s" % csr_filename) else: - csr = M2Crypto.X509.load_request_string(self.csr_file) - csr_pem, csr_der = csr.as_pem(), csr.as_der() + 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 @@ -675,18 +699,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_file and not crypto_util.valid_csr(self.csr_file): - raise Exception("The provided CSR is not a valid CSR") + if self.csr and not crypto_util.valid_csr(self.csr): + raise errors.LetsEncryptClientError("The provided CSR is not a " + "valid CSR") # If key is provided, it must be readable and valid. - if self.key_file and not crypto_util.valid_privkey(self.key_file): - raise Exception("The provided key is not a valid key") + if self.privkey and not crypto_util.valid_privkey(self.privkey): + 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_file and self.key_file: - if not crypto_util.csr_matches_pubkey(self.csr_file, self.key_file): - raise Exception("The key and CSR do not match") + if self.csr and self.privkey: + if not crypto_util.csr_matches_pubkey(self.csr, self.privkey): + raise errors.LetsEncryptClientError("The key and CSR do not " + "match") def get_all_names(self): """Return all valid names in the configuration.""" diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index e09368cbe..0c46bea6d 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -1,3 +1,4 @@ +"""Let's Encrypt client crypto utility functions""" import binascii import hashlib import time @@ -206,33 +207,33 @@ def get_cert_info(filename): # A. Do more checks to verify that the CSR is trusted/valid # B. Audit the parsing code for vulnerabilities -def valid_csr(csr_file): +def valid_csr(csr): """Validate CSR. - Check if `csr_filename` is a valid CSR for the given domains. + Check if `csr` is a valid CSR for the given domains. - :param csr_file: CSR file contents - :type csr_file: str + :param csr: CSR file contents + :type csr: str :returns: Validity of CSR. :rtype: bool """ try: - csr = M2Crypto.X509.load_request_string(csr_file) - return bool(csr.verify(csr.get_pubkey())) + csr_obj = M2Crypto.X509.load_request_string(csr) + return bool(csr_obj.verify(csr_obj.get_pubkey())) except M2Crypto.X509.X509Error: return False -def csr_matches_names(csr_file, domains): +def csr_matches_names(csr, domains): """Check if CSR contains the subject of one of the domains. M2Crypto currently does not expose the OpenSSL interface to also check the SAN extension. This is insufficient for full testing - :param csr_file: CSR file contents - :type csr_file: str + :param csr: CSR file contents + :type csr: str :param domains: Domains the CSR should contain. :type domains: list @@ -242,41 +243,41 @@ def csr_matches_names(csr_file, domains): """ try: - csr = M2Crypto.X509.load_request_string(csr_file) - return csr.get_subject().CN in domains + csr_obj = M2Crypto.X509.load_request_string(csr) + return csr_obj.get_subject().CN in domains except M2Crypto.X509.X509Error: return False -def valid_privkey(privkey_file): +def valid_privkey(privkey): """Is valid RSA private key? - :param privkey_file: Private key file contents - :type privkey_file: str + :param privkey: Private key file contents + :type privkey: str :returns: Validity of private key. :rtype: bool """ try: - return bool(M2Crypto.RSA.load_key_string(privkey_file).check_key()) + return bool(M2Crypto.RSA.load_key_string(privkey).check_key()) except M2Crypto.RSA.RSAError: return False -def csr_matches_pubkey(csr_file, privkey_file): +def csr_matches_pubkey(csr, privkey): """Does private key correspond to the subject public key in the CSR? - :param csr_file: CSR file contents - :type csr_file: str + :param csr: CSR file contents + :type csr: str - :param privkey_file: Private key file contents - :type privkey_file: str + :param privkey: Private key file contents + :type privkey: str :returns: Correspondence of private key to CSR subject public key. :rtype: bool """ - csr = M2Crypto.X509.load_request_string(csr_file) - privkey = M2Crypto.RSA.load_key_string(privkey_file) - return csr.get_pubkey().get_rsa().pub() == privkey.pub() + csr_obj = M2Crypto.X509.load_request_string(csr) + privkey_obj = M2Crypto.RSA.load_key_string(privkey) + return csr_obj.get_pubkey().get_rsa().pub() == privkey_obj.pub() diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index d379c5692..465e3a781 100755 --- a/letsencrypt/scripts/main.py +++ b/letsencrypt/scripts/main.py @@ -60,10 +60,7 @@ def main(): parser.add_argument("--test", dest="test", action="store_true", help="Run in test mode.") - try: - args = parser.parse_args() - except IOError as exc: - parser.error(exc) + args = parser.parse_args() # Enforce '--privkey' is set along with '--csr'. if args.csr and not args.privkey: @@ -101,19 +98,14 @@ def read_file(filename): :returns: File contents :rtype: str - :raise IOError: File does not exist or is not readable. file() will - potentially throw its own IOError exceptions in addition to the two - explicitely thrown. + + :raises argparse.ArgumentTypeError: File does not exist or is not readable. """ - - if not os.path.exists(filename): - raise IOError("the file '{0}' is not found".format(filename)) - - if not os.access(filename, os.R_OK): - raise IOError("the file '{0}' is not readable".format(filename)) - - return file(filename, 'rU').read() + try: + return file(filename, 'rU').read() + except IOError as exc: + raise argparse.ArgumentTypeError(exc.strerror) def rollback(config, checkpoints): From 9581c363b116f325ac5bad2b8bb4f9cd90d78e73 Mon Sep 17 00:00:00 2001 From: Adam Woodbeck Date: Sat, 29 Nov 2014 12:02:48 -0500 Subject: [PATCH 12/17] Fixed attribute reference and docstring. --- letsencrypt/client/client.py | 4 ++-- letsencrypt/client/le_util.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index f4b42f9a8..437d3d815 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -659,7 +659,7 @@ class Client(object): # Save file le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700) - key_f, self.key_file = le_util.unique_file( + key_f, key_filename = le_util.unique_file( os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600) key_f.write(key_pem) key_f.close() @@ -673,7 +673,7 @@ class Client(object): # Save CSR le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755) - csr_f, self.csr_file = le_util.unique_file( + csr_f, csr_filename = le_util.unique_file( os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644) csr_f.write(csr_pem) csr_f.close() diff --git a/letsencrypt/client/le_util.py b/letsencrypt/client/le_util.py index 5e8f5414b..70b9f5a86 100644 --- a/letsencrypt/client/le_util.py +++ b/letsencrypt/client/le_util.py @@ -51,7 +51,17 @@ def check_permissions(filepath, mode, uid=0): def unique_file(default_name, mode=0o777): - """Safely finds a unique file for writing only (by default).""" + """Safely finds a unique file for writing only (by default). + + :param default_name: Default file name + :type default_name: str + + :param mode: File mode + :type mode: int + + :return: tuple of file object and file name + + """ count = 1 f_parsed = os.path.splitext(default_name) while 1: From 1a25b3d7cdfec93f81cedf095581cb12421744b9 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 29 Nov 2014 21:59:46 +0100 Subject: [PATCH 13/17] Policies for pull requests. Coding style. --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3489a36b8..df800854d 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,49 @@ sudo ./venv/bin/letsencrypt ## Hacking -1. Bootstrap: `./venv/bin/python setup.py dev` +In order to start hacking, you will first have to create a development +environment: -2. Test code base: `./venv/bin/tox` +`./venv/bin/python setup.py dev` + +The code base, including your pull requests, **must have 100% test +statement coverage and be compliant with the [coding +style](#coding-style)**. The following tools are there to help you: + +- `./venv/bin/tox` starts a full set of tests. Please make sure you + run it before submitting a new pull request. + +- `./venv/bin/tox -e cover` checks the test coverage only. + +- `./venv/bin/tox -e lint` checks the style of the whole project, + while `./venv/bin/pylint file` will check a single `file` only. + +### Coding style + +Most importantly, *be consistent with the rest of the code*, please. + +1. Read [PEP 8 - Style Guide for PythonCode] +(https://www.python.org/dev/peps/pep-0008). + +2. Follow [Google Python Style Guide] +(https://google-styleguide.googlecode.com/svn/trunk/pyguide.html), +with the following exception that we use +[Sphinx](http://sphinx-doc.org/)-style documentation: + + ```python + def foo(arg): + """Short description. + + :param int arg: Some number. + + :returns: Argument + :rtype: int + + """ + return arg + ``` + +3. Remember to use `./venv/bin/pylint`. ## Command line usage From 9bc369f5a9138189e5da77ef76e161bc869b526e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 29 Nov 2014 22:10:30 +0100 Subject: [PATCH 14/17] Fix markdown bold fail --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df800854d..241e1e8ed 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ style](#coding-style)**. The following tools are there to help you: ### Coding style -Most importantly, *be consistent with the rest of the code*, please. +Most importantly, **be consistent with the rest of the code**, please. 1. Read [PEP 8 - Style Guide for PythonCode] (https://www.python.org/dev/peps/pep-0008). From 49413cb9d239c7fca65931427d1346574cf16ebf Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 29 Nov 2014 22:11:40 +0100 Subject: [PATCH 15/17] README: -following --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 241e1e8ed..2516c79b6 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,8 @@ Most importantly, **be consistent with the rest of the code**, please. 2. Follow [Google Python Style Guide] (https://google-styleguide.googlecode.com/svn/trunk/pyguide.html), -with the following exception that we use -[Sphinx](http://sphinx-doc.org/)-style documentation: +with the exception that we use [Sphinx](http://sphinx-doc.org/)-style +documentation: ```python def foo(arg): From f116a8fc8eac8f60c3a0537fef0247fcbb4bc1de Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 29 Nov 2014 22:12:20 +0100 Subject: [PATCH 16/17] Add missing whitespace to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2516c79b6..3b763fd58 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ style](#coding-style)**. The following tools are there to help you: Most importantly, **be consistent with the rest of the code**, please. -1. Read [PEP 8 - Style Guide for PythonCode] +1. Read [PEP 8 - Style Guide for Python Code] (https://www.python.org/dev/peps/pep-0008). 2. Follow [Google Python Style Guide] From d2a1c969e460846706b5a676d411291791f0c70f Mon Sep 17 00:00:00 2001 From: James Kasten Date: Sat, 29 Nov 2014 16:05:18 -0800 Subject: [PATCH 17/17] Added in filename support because configuration files still need to reference the correct place --- letsencrypt/client/apache_configurator.py | 13 ++++++++++--- letsencrypt/client/client.py | 23 ++++++++++++++--------- letsencrypt/client/crypto_util.py | 19 ++++++++++--------- letsencrypt/client/errors.py | 4 ++++ letsencrypt/scripts/main.py | 17 ++++++++++++----- 5 files changed, 50 insertions(+), 26 deletions(-) diff --git a/letsencrypt/client/apache_configurator.py b/letsencrypt/client/apache_configurator.py index c8dd4df9a..f5ebf6be7 100644 --- a/letsencrypt/client/apache_configurator.py +++ b/letsencrypt/client/apache_configurator.py @@ -13,6 +13,7 @@ from Crypto import Random from letsencrypt.client import augeas_configurator from letsencrypt.client import CONFIG from letsencrypt.client import crypto_util +from letsencrypt.client import errors from letsencrypt.client import le_util from letsencrypt.client import logger @@ -1536,7 +1537,7 @@ LogLevel warn \n\ self.add_dir("/files" + mainConfig, "Include", CONFIG.APACHE_CHALLENGE_CONF) - def dvsni_create_chall_cert(self, name, ext, nonce, key): + def dvsni_create_chall_cert(self, name, ext, nonce, key_file): """Creates DVSNI challenge certifiate. Certificate created at dvsni_get_cert_file(nonce) @@ -1544,14 +1545,20 @@ LogLevel warn \n\ :param nonce: hex form of nonce :type nonce: str - :param key: file path to key + :param key_file: absolute path to key file :type key: str """ + try: + with open(key_file, 'r') as key_fd: + key_str = key_fd.read() + except IOError: + raise LetsEncryptDvsniError("Unable to load key file: %s" % key) + self.register_file_creation(True, self.dvsni_get_cert_file(nonce)) cert_pem = crypto_util.make_ss_cert( - key, [nonce + CONFIG.INVALID_EXT, name, ext]) + key_str, [nonce + CONFIG.INVALID_EXT, name, ext]) with open(self.dvsni_get_cert_file(nonce), 'w') as f: f.write(cert_pem) diff --git a/letsencrypt/client/client.py b/letsencrypt/client/client.py index 437d3d815..6b625fc37 100644 --- a/letsencrypt/client/client.py +++ b/letsencrypt/client/client.py @@ -33,7 +33,7 @@ class Client(object): """ACME protocol client.""" def __init__(self, ca_server, cert_signing_request=None, - private_key=None, use_curses=True): + private_key=None, private_key_file=None, use_curses=True): """ :param ca_server: Certificate authority server @@ -45,6 +45,9 @@ class Client(object): :param private_key: Contents of the private key :type private_key: str + :param private_key_file: absolute path to private_key + :type private_key_file: str + :param use_curses: Use curses UI :type use_curses: bool @@ -61,6 +64,7 @@ class Client(object): self.server = ca_server self.csr = cert_signing_request self.privkey = private_key + self.privkey_file = private_key_file # TODO: Figure out all exceptions from this function try: @@ -396,7 +400,6 @@ class Client(object): else: self.choose_certs(certs) elif code == display.HELP: - print code, tag, cert display.more_info_cert(cert) self.choose_certs(certs) else: @@ -431,7 +434,7 @@ class Client(object): for host in vhost: self.config.deploy_cert(host, os.path.abspath(cert_file), - os.path.abspath(self.privkey), + os.path.abspath(self.privkey_file), cert_chain_abspath) # Enable any vhost that was issued to, but not enabled if not host.enabled: @@ -542,17 +545,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]) + 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]) + csvwriter.writerow(["0", cert_file, self.privkey_file]) - shutil.copy2(self.privkey, + shutil.copy2(self.privkey_file, os.path.join( CONFIG.CERT_KEY_BACKUP, - os.path.basename(self.privkey) + "_" + str(idx))) + os.path.basename(self.privkey_file) + "_" + str(idx))) shutil.copy2(cert_file, os.path.join( CONFIG.CERT_KEY_BACKUP, @@ -628,7 +631,7 @@ class Client(object): challenge_objs.append({ "type": "dvsni", "listSNITuple": sni_todo, - "dvsni_key": os.path.abspath(self.privkey), + "dvsni_key": os.path.abspath(self.privkey_file), }) challenge_obj_indices.append(sni_satisfies) logger.debug(sni_todo) @@ -663,7 +666,9 @@ class Client(object): os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600) key_f.write(key_pem) key_f.close() - logger.info("Generating key: %s" % key_filename) + + self.privkey_file = key_filename + logger.info("Generating key: %s" % self.privkey_file) else: key_pem = self.privkey diff --git a/letsencrypt/client/crypto_util.py b/letsencrypt/client/crypto_util.py index 0c46bea6d..f2bb4c6f7 100644 --- a/letsencrypt/client/crypto_util.py +++ b/letsencrypt/client/crypto_util.py @@ -22,7 +22,7 @@ def b64_cert_to_pem(b64_der_cert): le_util.jose_b64decode(b64_der_cert)).as_pem() -def create_sig(msg, key_file, nonce=None, nonce_len=CONFIG.NONCE_SIZE): +def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE): """Create signature with nonce prepended to the message. TODO: Change this over to M2Crypto... PKey @@ -32,9 +32,9 @@ def create_sig(msg, key_file, nonce=None, nonce_len=CONFIG.NONCE_SIZE): :param msg: Message to be signed :type msg: Anything with __str__ method - :param key_file: Path to a file containing RSA key. Accepted formats + :param key_str: Key in string form. Accepted formats are the same as for `Crypto.PublicKey.RSA.importKey`. - :type key_file: str + :type key_str: str :param nonce: Nonce to be used. If None, nonce of `nonce_len` size will be randomly genereted. @@ -48,7 +48,7 @@ def create_sig(msg, key_file, nonce=None, nonce_len=CONFIG.NONCE_SIZE): """ msg = str(msg) - key = Crypto.PublicKey.RSA.importKey(open(key_file).read()) + key = Crypto.PublicKey.RSA.importKey(key_str) nonce = Random.get_random_bytes(nonce_len) if nonce is None else nonce msg_with_nonce = nonce + msg @@ -96,12 +96,12 @@ def make_key(bits=CONFIG.RSA_KEY_SIZE): return key.exportKey(format='PEM') -def make_csr(key_file, domains): +def make_csr(key_str, domains): """ Returns new CSR in PEM and DER form using key_file containing all domains """ assert domains, "Must provide one or more hostnames for the CSR." - rsa_key = M2Crypto.RSA.load_key(key_file) + rsa_key = M2Crypto.RSA.load_key_string(key_str) pubkey = M2Crypto.EVP.PKey() pubkey.assign_rsa(rsa_key) @@ -128,13 +128,14 @@ def make_csr(key_file, domains): return csr.as_pem(), csr.as_der() -def make_ss_cert(key_file, domains): +def make_ss_cert(key_str, domains): """Returns new self-signed cert in PEM form. - Uses key_file and contains all domains. + Uses key_str and contains all domains. """ assert domains, "Must provide one or more hostnames for the CSR." - rsa_key = M2Crypto.RSA.load_key(key_file) + + rsa_key = M2Crypto.RSA.load_key_string(key_str) pubkey = M2Crypto.EVP.PKey() pubkey.assign_rsa(rsa_key) diff --git a/letsencrypt/client/errors.py b/letsencrypt/client/errors.py index 288da35cf..b0f5b769b 100644 --- a/letsencrypt/client/errors.py +++ b/letsencrypt/client/errors.py @@ -3,3 +3,7 @@ class LetsEncryptClientError(Exception): """Generic Let's Encrypt client error.""" + + +class LetsEncryptDvsniError(Exception): + """Let's Encrypt DVSNI error.""" diff --git a/letsencrypt/scripts/main.py b/letsencrypt/scripts/main.py index 465e3a781..d38762925 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", type=read_file, + parser.add_argument("-p", "--privkey", dest="privkey_tup", type=read_file, help="Path to the private key file for certificate " "generation.") - parser.add_argument("-c", "--csr", dest="csr", type=read_file, + parser.add_argument("-c", "--csr", dest="csr_tup", 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 and not args.privkey: + if args.csr_tup and not args.privkey_tup: parser.error("private key file (--privkey) must be specified along{0} " "with the certificate signing request file (--csr)" .format(os.linesep)) @@ -83,7 +83,14 @@ def main(): server = args.server is None and CONFIG.ACME_SERVER or args.server - acme = client.Client(server, args.csr, args.privkey, args.curses) + # 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) + + acme = client.Client(server, args.csr_tup[1], args.privkey_tup[1], + args.privkey_tup[0], args.curses) if args.revoke: acme.list_certs_keys() else: @@ -103,7 +110,7 @@ def read_file(filename): """ try: - return file(filename, 'rU').read() + return filename, file(filename, 'rU').read() except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror)