mirror of
https://github.com/certbot/certbot.git
synced 2026-06-06 15:22:38 -04:00
Merge remote-tracking branch 'github/letsencrypt/master' into bugs/44
Conflicts: letsencrypt/client/client.py
This commit is contained in:
commit
8f6d4b4344
7 changed files with 240 additions and 140 deletions
47
README.md
47
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 Python Code]
|
||||
(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 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
|
||||
|
||||
|
|
@ -78,8 +118,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 <ROLLBACK> 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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""ACME protocol client class and helper functions."""
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
|
|
@ -32,7 +33,25 @@ 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
|
||||
: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 private_key_file: absolute path to private_key
|
||||
:type private_key_file: str
|
||||
|
||||
:param use_curses: Use curses UI
|
||||
:type use_curses: bool
|
||||
|
||||
"""
|
||||
self.curses = use_curses
|
||||
|
||||
# Logger needs to be initialized before Configurator
|
||||
|
|
@ -42,21 +61,15 @@ 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.name
|
||||
else:
|
||||
self.csr_file = None
|
||||
if private_key:
|
||||
self.key_file = private_key.name
|
||||
else:
|
||||
self.key_file = None
|
||||
self.csr = cert_signing_request
|
||||
self.privkey = private_key
|
||||
self.privkey_file = private_key_file
|
||||
|
||||
# TODO: Figure out all exceptions from this function
|
||||
try:
|
||||
self._validate_csr_key_cli()
|
||||
|
||||
except errors.LetsEncryptClientError as e:
|
||||
# TODO: Something nice here...
|
||||
logger.fatal("%s - until the programmers get their act together, "
|
||||
|
|
@ -65,6 +78,21 @@ 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
|
||||
|
||||
:raises errors.LetsEncryptClientError: CSR does not contain one of the
|
||||
specified names.
|
||||
|
||||
"""
|
||||
domains = [] if domains is None else domains
|
||||
|
||||
# Check configuration
|
||||
|
|
@ -155,7 +183,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")
|
||||
|
|
@ -178,7 +206,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.
|
||||
|
|
@ -211,14 +239,14 @@ class Client(object):
|
|||
:param msg: ACME message (JSON serializable).
|
||||
:type msg: dict
|
||||
|
||||
:raises TypeError: if `msg` is not JSON serializable
|
||||
:raises jsonschema.ValidationError: if `msg` is not valid ACME message
|
||||
:raises LetsEncryptClientError: in case of a connection error
|
||||
or if a 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)
|
||||
|
|
@ -248,7 +276,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
|
||||
|
|
@ -256,6 +284,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:
|
||||
|
|
@ -281,11 +311,11 @@ class Client(object):
|
|||
reponse message.
|
||||
:type rounds: int
|
||||
|
||||
:raises LetsEncryptClientError: if server sent ACME "error" message
|
||||
|
||||
:returns: ACME response message from server.
|
||||
:rtype: dict
|
||||
|
||||
:raises LetsEncryptClientError: if server sent ACME "error" message
|
||||
|
||||
"""
|
||||
for _ in xrange(rounds):
|
||||
if response["type"] == expected:
|
||||
|
|
@ -312,6 +342,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 = []
|
||||
|
||||
|
|
@ -370,7 +401,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:
|
||||
|
|
@ -405,7 +435,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_file),
|
||||
cert_chain_abspath)
|
||||
# Enable any vhost that was issued to, but not enabled
|
||||
if not host.enabled:
|
||||
|
|
@ -496,6 +526,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, 0o700)
|
||||
|
|
@ -513,22 +546,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_file])
|
||||
|
||||
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_file])
|
||||
|
||||
shutil.copy2(self.key_file,
|
||||
shutil.copy2(self.privkey_file,
|
||||
os.path.join(
|
||||
CONFIG.CERT_KEY_BACKUP,
|
||||
os.path.basename(self.key_file) + "_" + str(idx)))
|
||||
os.path.basename(self.privkey_file) + "_" + 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)
|
||||
|
|
@ -552,7 +587,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
|
||||
|
|
@ -597,7 +632,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_file),
|
||||
})
|
||||
challenge_obj_indices.append(sni_satisfies)
|
||||
logger.debug(sni_todo)
|
||||
|
|
@ -622,40 +657,36 @@ class Client(object):
|
|||
:rtype: tuple
|
||||
|
||||
"""
|
||||
key_pem = None
|
||||
csr_pem = None
|
||||
if not self.key_file:
|
||||
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, 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()
|
||||
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)
|
||||
|
||||
if not self.csr_file:
|
||||
csr_pem, csr_der = crypto_util.make_csr(self.key_file, self.names)
|
||||
self.privkey_file = key_filename
|
||||
logger.info("Generating key: %s" % self.privkey_file)
|
||||
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
|
||||
|
||||
# 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()
|
||||
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_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,38 +706,22 @@ 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 errors.LetsEncryptClientError(
|
||||
"The provided CSR is not a valid CSR")
|
||||
except IOError:
|
||||
if self.csr and not crypto_util.valid_csr(self.csr):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The provided CSR could not be read")
|
||||
"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 LetsEncryptClientError(
|
||||
"The provided key is not a valid key")
|
||||
except IOError:
|
||||
raise LetsEncryptClientError("The provided key could not be read")
|
||||
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:
|
||||
try:
|
||||
if not crypto_util.csr_matches_pubkey(
|
||||
self.csr_file, self.key_file):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The key and CSR do not match")
|
||||
except IOError:
|
||||
if self.csr and self.privkey:
|
||||
if not crypto_util.csr_matches_pubkey(self.csr, self.privkey):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The key or CSR files could not be read")
|
||||
"The key and CSR do not match")
|
||||
|
||||
def get_all_names(self):
|
||||
"""Return all valid names in the configuration."""
|
||||
|
|
@ -760,6 +775,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")
|
||||
|
|
@ -767,9 +788,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 + "-."
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
"""Let's Encrypt client crypto utility functions"""
|
||||
import binascii
|
||||
import hashlib
|
||||
import time
|
||||
|
|
@ -21,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
|
||||
|
|
@ -31,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.
|
||||
|
|
@ -47,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
|
||||
|
|
@ -95,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)
|
||||
|
||||
|
|
@ -127,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)
|
||||
|
||||
|
|
@ -206,39 +208,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):
|
||||
"""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.
|
||||
|
||||
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: CSR file contents
|
||||
:type csr: str
|
||||
|
||||
:returns: Validity of CSR.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
try:
|
||||
csr = M2Crypto.X509.load_request(csr_filename)
|
||||
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_filename, 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
|
||||
|
||||
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: CSR file contents
|
||||
:type csr: str
|
||||
|
||||
:param domains: Domains the CSR should contain.
|
||||
:type domains: list
|
||||
|
|
@ -248,49 +244,41 @@ def csr_matches_names(csr_filename, domains):
|
|||
|
||||
"""
|
||||
try:
|
||||
csr = M2Crypto.X509.load_request(csr_filename)
|
||||
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_filename):
|
||||
def valid_privkey(privkey):
|
||||
"""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: Private key file contents
|
||||
:type privkey: 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).check_key())
|
||||
except M2Crypto.RSA.RSAError:
|
||||
return False
|
||||
|
||||
|
||||
def csr_matches_pubkey(csr_filename, privkey_filename):
|
||||
def csr_matches_pubkey(csr, privkey):
|
||||
"""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: CSR file contents
|
||||
:type csr: 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: Private key file contents
|
||||
:type privkey: 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)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -3,3 +3,7 @@
|
|||
|
||||
class LetsEncryptClientError(Exception):
|
||||
"""Generic Let's Encrypt client error."""
|
||||
|
||||
|
||||
class LetsEncryptDvsniError(Exception):
|
||||
"""Let's Encrypt DVSNI error."""
|
||||
|
|
|
|||
|
|
@ -55,7 +55,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:
|
||||
|
|
|
|||
|
|
@ -28,18 +28,17 @@ 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_tup", type=read_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_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 "
|
||||
"argument is specified.")
|
||||
parser.add_argument("-b", "--rollback", dest="rollback", type=int,
|
||||
default=0,
|
||||
help="Revert configuration <ROLLBACK> 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",
|
||||
|
|
@ -63,10 +62,9 @@ def main():
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 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{}"
|
||||
# Enforce '--privkey' is set along with '--csr'.
|
||||
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))
|
||||
|
||||
|
|
@ -85,13 +83,38 @@ 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:
|
||||
acme.authenticate(args.domains, args.redirect, args.eula)
|
||||
|
||||
|
||||
def read_file(filename):
|
||||
"""Returns the given file's contents with universal new line support.
|
||||
|
||||
:param filename: Filename
|
||||
:type filename: str
|
||||
|
||||
:returns: File contents
|
||||
:rtype: str
|
||||
|
||||
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
|
||||
|
||||
"""
|
||||
try:
|
||||
return filename, file(filename, 'rU').read()
|
||||
except IOError as exc:
|
||||
raise argparse.ArgumentTypeError(exc.strerror)
|
||||
|
||||
|
||||
def rollback(config, checkpoints):
|
||||
"""Revert configuration the specified number of checkpoints.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue