Added namedtuples for key, csr in client

This commit is contained in:
James Kasten
2014-11-30 02:31:44 -08:00
parent 384e01a3d8
commit 8a62587a6e
4 changed files with 81 additions and 86 deletions
+22 -25
View File
@@ -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 = "<IfModule mod_ssl.c> \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 += "</IfModule> \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"
"</VirtualHost> \n\n")
+46 -51
View File
@@ -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")
+1 -1
View File
@@ -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
+12 -9
View File
@@ -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: