1
0
mirror of https://github.com/moepman/acertmgr.git synced 2024-12-29 10:31:49 +01:00

tools: add log function, update log messages mentioning certificates

This simple implementation writes log messages to stdout/err and flushes
the buffers immediately after the message has been written.

Also update log messages with the certificate CN to a better readable format
Introduce functions for get_cert_cn and get_cert_valid_until to
encapsulate all cryptographic functions consistently in tools.
This commit is contained in:
Kishi85 2019-04-04 13:15:34 +02:00
parent 2046215e37
commit b63a0bc424
9 changed files with 120 additions and 67 deletions

View File

@ -16,12 +16,13 @@ import subprocess
from acertmgr import configuration, tools from acertmgr import configuration, tools
from acertmgr.authority import authority from acertmgr.authority import authority
from acertmgr.modes import challenge_handler from acertmgr.modes import challenge_handler
from acertmgr.tools import log
# @brief fetch new certificate from letsencrypt # @brief fetch new certificate from letsencrypt
# @param settings the domain's configuration options # @param settings the domain's configuration options
def cert_get(settings): def cert_get(settings):
print("Getting certificate for %s" % settings['domainlist']) log("Getting certificate for %s" % settings['domainlist'])
acme = authority(settings['authority']) acme = authority(settings['authority'])
acme.register_account() acme.register_account()
@ -38,16 +39,16 @@ def cert_get(settings):
if os.path.isfile(key_file): if os.path.isfile(key_file):
key = tools.read_pem_file(key_file, key=True) key = tools.read_pem_file(key_file, key=True)
else: else:
print("SSL key not found at '{0}'. Creating {1} bit key.".format(key_file, key_length)) log("SSL key not found at '{0}'. Creating {1} bit key.".format(key_file, key_length))
key = tools.new_ssl_key(key_file, key_length) key = tools.new_ssl_key(key_file, key_length)
# create ssl csr # create ssl csr
csr_file = settings['csr_file'] csr_file = settings['csr_file']
if os.path.isfile(csr_file) and str(settings['csr_static']).lower() == 'true': if os.path.isfile(csr_file) and str(settings['csr_static']).lower() == 'true':
print('Loading CSR from {}'.format(csr_file)) log('Loading CSR from {}'.format(csr_file))
cr = tools.read_pem_file(csr_file, csr=True) cr = tools.read_pem_file(csr_file, csr=True)
else: else:
print('Generating CSR for {}'.format(settings['domainlist'])) log('Generating CSR for {}'.format(settings['domainlist']))
must_staple = str(settings.get('cert_must_staple')).lower() == "true" must_staple = str(settings.get('cert_must_staple')).lower() == "true"
cr = tools.new_cert_request(settings['domainlist'], key, must_staple) cr = tools.new_cert_request(settings['domainlist'], key, must_staple)
tools.write_pem_file(cr, csr_file) tools.write_pem_file(cr, csr_file)
@ -57,7 +58,8 @@ def cert_get(settings):
# if resulting certificate is valid: store in final location # if resulting certificate is valid: store in final location
if tools.is_cert_valid(crt, settings['ttl_days']): if tools.is_cert_valid(crt, settings['ttl_days']):
print("Certificate '{}' renewed and valid until {}".format(crt, crt.not_valid_after)) log("Certificate '{}' renewed and valid until {}".format(tools.get_cert_cn(crt),
tools.get_cert_valid_until(crt)))
tools.write_pem_file(crt, settings['cert_file'], stat.S_IREAD) tools.write_pem_file(crt, settings['cert_file'], stat.S_IREAD)
if (not str(settings.get('ca_static')).lower() == 'true' or not os.path.exists(settings['ca_file'])) \ if (not str(settings.get('ca_static')).lower() == 'true' or not os.path.exists(settings['ca_file'])) \
and ca is not None: and ca is not None:
@ -107,11 +109,11 @@ def cert_put(settings):
try: try:
os.chown(crt_path, uid, gid) os.chown(crt_path, uid, gid)
except OSError: except OSError:
print('Warning: Could not set certificate file ownership!') log('Could not set certificate file ownership!', warning=True)
try: try:
os.chmod(crt_path, int(crt_perm, 8)) os.chmod(crt_path, int(crt_perm, 8))
except OSError: except OSError:
print('Warning: Could not set certificate file permissions!') log('Could not set certificate file permissions!', warning=True)
return crt_action return crt_action
@ -131,7 +133,7 @@ def main():
runtimeconfig, domainconfigs = configuration.load() runtimeconfig, domainconfigs = configuration.load()
if runtimeconfig.get('mode') == 'revoke': if runtimeconfig.get('mode') == 'revoke':
# Mode: revoke certificate # Mode: revoke certificate
print("Revoking {}".format(runtimeconfig['revoke'])) log("Revoking {}".format(runtimeconfig['revoke']))
cert_revoke(tools.read_pem_file(runtimeconfig['revoke']), domainconfigs, runtimeconfig['revoke_reason']) cert_revoke(tools.read_pem_file(runtimeconfig['revoke']), domainconfigs, runtimeconfig['revoke_reason'])
else: else:
# Mode: issue certificates (implicit) # Mode: issue certificates (implicit)
@ -152,7 +154,7 @@ def main():
if str(config.get('cert_revoke_superseded')).lower() == 'true' and cert: if str(config.get('cert_revoke_superseded')).lower() == 'true' and cert:
superseded.add(cert) superseded.add(cert)
except Exception as e: except Exception as e:
print("Certificate issue/renew failed: {}".format(e)) log("Certificate issue/renew failed", e, error=True)
exceptions.append(e) exceptions.append(e)
# deploy new certificates after all are renewed # deploy new certificates after all are renewed
@ -161,10 +163,10 @@ def main():
try: try:
for cfg in config['actions']: for cfg in config['actions']:
if not tools.target_is_current(cfg['path'], config['cert_file']): if not tools.target_is_current(cfg['path'], config['cert_file']):
print("Updating '{}' due to newer version".format(cfg['path'])) log("Updating '{}' due to newer version".format(cfg['path']))
actions.add(cert_put(cfg)) actions.add(cert_put(cfg))
except Exception as e: except Exception as e:
print("Certificate deployment failed: {}".format(e)) log("Certificate deployment failed", e, error=True)
exceptions.append(e) exceptions.append(e)
deployment_success = False deployment_success = False
@ -174,9 +176,10 @@ def main():
try: try:
# Run actions in a shell environment (to allow shell syntax) as stated in the configuration # Run actions in a shell environment (to allow shell syntax) as stated in the configuration
output = subprocess.check_output(action, shell=True, stderr=subprocess.STDOUT) output = subprocess.check_output(action, shell=True, stderr=subprocess.STDOUT)
print("Executed '{}' successfully: {}".format(action, output)) log("Executed '{}' successfully: {}".format(action, output))
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print("Execution of '{}' failed with error '{}': {}".format(e.cmd, e.returncode, e.output)) log("Execution of '{}' failed with error '{}': {}".format(e.cmd, e.returncode, e.output), e,
error=True)
exceptions.append(e) exceptions.append(e)
deployment_success = False deployment_success = False
@ -184,14 +187,14 @@ def main():
if deployment_success: if deployment_success:
for superseded_cert in superseded: for superseded_cert in superseded:
try: try:
print("Revoking previous certificate '{}' valid until {} as superseded".format( log("Revoking '{}' valid until {} as superseded".format(
superseded_cert, tools.get_cert_cn(superseded_cert),
superseded_cert.not_valid_after)) tools.get_cert_valid_until(superseded_cert)))
cert_revoke(superseded_cert, domainconfigs, reason=4) # reason=4 is superseded cert_revoke(superseded_cert, domainconfigs, reason=4) # reason=4 is superseded
except Exception as e: except Exception as e:
print("Certificate supersede revoke failed: {}".format(e)) log("Certificate supersede revoke failed", e, error=True)
exceptions.append(e) exceptions.append(e)
# throw a RuntimeError with all exceptions caught while working if there were any # throw a RuntimeError with all exceptions caught while working if there were any
if len(exceptions) > 0: if len(exceptions) > 0:
raise RuntimeError("{} exception(s) occurred during runtime: {}".format(len(exceptions), exceptions)) raise RuntimeError("{} exception(s) occurred during processing".format(len(exceptions)))

View File

@ -10,6 +10,7 @@ import json
import os import os
from acertmgr import tools from acertmgr import tools
from acertmgr.tools import log
authorities = dict() authorities = dict()
@ -23,10 +24,10 @@ def authority(settings):
else: else:
acc_file = settings['account_key'] acc_file = settings['account_key']
if os.path.isfile(acc_file): if os.path.isfile(acc_file):
print("Reading account key from {}".format(acc_file)) log("Reading account key from {}".format(acc_file))
acc_key = tools.read_pem_file(acc_file, key=True) acc_key = tools.read_pem_file(acc_file, key=True)
else: else:
print("Account key not found at '{0}'. Creating key.".format(acc_file)) log("Account key not found at '{0}'. Creating key.".format(acc_file))
acc_key = tools.new_account_key(acc_file) acc_key = tools.new_account_key(acc_file)
authority_module = importlib.import_module("acertmgr.authority.{0}".format(settings["api"])) authority_module = importlib.import_module("acertmgr.authority.{0}".format(settings["api"]))

View File

@ -13,6 +13,7 @@ import time
from acertmgr import tools from acertmgr import tools
from acertmgr.authority.acme import ACMEAuthority as AbstractACMEAuthority from acertmgr.authority.acme import ACMEAuthority as AbstractACMEAuthority
from acertmgr.tools import log
class ACMEAuthority(AbstractACMEAuthority): class ACMEAuthority(AbstractACMEAuthority):
@ -70,11 +71,11 @@ class ACMEAuthority(AbstractACMEAuthority):
"agreement": self.agreement, "agreement": self.agreement,
}) })
if code == 201: if code == 201:
print("Registered!") log("Registered!")
self.registered_account = True self.registered_account = True
return True return True
elif code == 409: elif code == 409:
print("Already registered!") log("Already registered!")
self.registered_account = True self.registered_account = True
return False return False
else: else:
@ -96,7 +97,7 @@ class ACMEAuthority(AbstractACMEAuthority):
# verify each domain # verify each domain
try: try:
for domain in domains: for domain in domains:
print("Verifying {0}...".format(domain)) log("Verifying {0}...".format(domain))
# get new challenge # get new challenge
code, result = self._send_signed(self.ca + "/acme/new-authz", header, { code, result = self._send_signed(self.ca + "/acme/new-authz", header, {
@ -120,7 +121,7 @@ class ACMEAuthority(AbstractACMEAuthority):
for domain in domains: for domain in domains:
challenge_handlers[domain].start_challenge(domain, account_thumbprint, tokens[domain]) challenge_handlers[domain].start_challenge(domain, account_thumbprint, tokens[domain])
try: try:
print("Starting key authorization") log("Starting key authorization")
# notify challenge are met # notify challenge are met
keyauthorization = "{0}.{1}".format(tokens[domain], account_thumbprint) keyauthorization = "{0}.{1}".format(tokens[domain], account_thumbprint)
code, result = self._send_signed(challenges[domain]['uri'], header, { code, result = self._send_signed(challenges[domain]['uri'], header, {
@ -141,7 +142,7 @@ class ACMEAuthority(AbstractACMEAuthority):
if challenge_status['status'] == "pending": if challenge_status['status'] == "pending":
time.sleep(2) time.sleep(2)
elif challenge_status['status'] == "valid": elif challenge_status['status'] == "valid":
print("{0} verified!".format(domain)) log("{0} verified!".format(domain))
break break
else: else:
raise ValueError("{0} challenge did not pass: {1}".format( raise ValueError("{0} challenge did not pass: {1}".format(
@ -155,10 +156,10 @@ class ACMEAuthority(AbstractACMEAuthority):
try: try:
challenge_handlers[domain].destroy_challenge(domain, account_thumbprint, tokens[domain]) challenge_handlers[domain].destroy_challenge(domain, account_thumbprint, tokens[domain])
except Exception as e: except Exception as e:
print('Challenge destruction failed: {}'.format(e)) log('Challenge destruction failed: {}'.format(e), error=True)
# get the new certificate # get the new certificate
print("Signing certificate...") log("Signing certificate...")
code, result = self._send_signed(self.ca + "/acme/new-cert", header, { code, result = self._send_signed(self.ca + "/acme/new-cert", header, {
"resource": "new-cert", "resource": "new-cert",
"csr": tools.bytes_to_base64url(tools.convert_cert_to_der_bytes(csr)), "csr": tools.bytes_to_base64url(tools.convert_cert_to_der_bytes(csr)),
@ -167,7 +168,7 @@ class ACMEAuthority(AbstractACMEAuthority):
raise ValueError("Error signing certificate: {0} {1}".format(code, result)) raise ValueError("Error signing certificate: {0} {1}".format(code, result))
# return signed certificate! # return signed certificate!
print("Certificate signed!") log("Certificate signed!")
cert = tools.convert_der_bytes_to_cert(result) cert = tools.convert_der_bytes_to_cert(result)
return cert, tools.download_issuer_ca(cert) return cert, tools.download_issuer_ca(cert)
@ -181,6 +182,6 @@ class ACMEAuthority(AbstractACMEAuthority):
payload['reason'] = int(reason) payload['reason'] = int(reason)
code, result = self._send_signed(self.ca + "/acme/revoke-cert", header, payload) code, result = self._send_signed(self.ca + "/acme/revoke-cert", header, payload)
if code < 400: if code < 400:
print("Revocation successful") log("Revocation successful")
else: else:
raise ValueError("Revocation failed: {}".format(result)) raise ValueError("Revocation failed: {}".format(result))

View File

@ -12,6 +12,7 @@ import time
from acertmgr import tools from acertmgr import tools
from acertmgr.authority.acme import ACMEAuthority as AbstractACMEAuthority from acertmgr.authority.acme import ACMEAuthority as AbstractACMEAuthority
from acertmgr.tools import log
class ACMEAuthority(AbstractACMEAuthority): class ACMEAuthority(AbstractACMEAuthority):
@ -41,7 +42,8 @@ class ACMEAuthority(AbstractACMEAuthority):
"newOrder": "{}/acme/new-order".format(self.ca), "newOrder": "{}/acme/new-order".format(self.ca),
"revokeCert": "{}/acme/revoke-cert".format(self.ca), "revokeCert": "{}/acme/revoke-cert".format(self.ca),
} }
print("API directory retrieval failed ({}). Guessed necessary values: {}".format(code, self.directory)) log("API directory retrieval failed ({}). Guessed necessary values: {}".format(code, self.directory),
warning=True)
self.nonce = None self.nonce = None
self.algorithm, jwk = tools.get_key_alg_and_jwk(key) self.algorithm, jwk = tools.get_key_alg_and_jwk(key)
@ -127,8 +129,8 @@ class ACMEAuthority(AbstractACMEAuthority):
if code < 400 and result['status'] == 'valid': if code < 400 and result['status'] == 'valid':
self.account_id = headers['Location'] self.account_id = headers['Location']
if 'meta' in self.directory and 'termsOfService' in self.directory['meta']: if 'meta' in self.directory and 'termsOfService' in self.directory['meta']:
print("ToS at {} have been accepted.".format(self.directory['meta']['termsOfService'])) log("ToS at {} have been accepted.".format(self.directory['meta']['termsOfService']))
print("Account registered and valid.".format()) log("Account registered and valid on {}.".format(self.ca))
else: else:
raise ValueError("Error registering account: {0} {1}".format(code, result)) raise ValueError("Error registering account: {0} {1}".format(code, result))
@ -142,7 +144,7 @@ class ACMEAuthority(AbstractACMEAuthority):
account_thumbprint = tools.bytes_to_base64url( account_thumbprint = tools.bytes_to_base64url(
tools.hash_of_str(json.dumps(self.account_protected['jwk'], sort_keys=True, separators=(',', ':')))) tools.hash_of_str(json.dumps(self.account_protected['jwk'], sort_keys=True, separators=(',', ':'))))
print("Ordering certificate for {}".format(domains)) log("Ordering certificate for {}".format(domains))
identifiers = [{'type': 'dns', 'value': domain} for domain in domains] identifiers = [{'type': 'dns', 'value': domain} for domain in domains]
code, order, headers = self._request_acme_endpoint('newOrder', {'identifiers': identifiers}) code, order, headers = self._request_acme_endpoint('newOrder', {'identifiers': identifiers})
if code >= 400: if code >= 400:
@ -160,7 +162,7 @@ class ACMEAuthority(AbstractACMEAuthority):
authorization['_domain'] = "*.{}".format(authorization['identifier']['value']) if \ authorization['_domain'] = "*.{}".format(authorization['identifier']['value']) if \
'wildcard' in authorization and authorization['wildcard'] else authorization['identifier']['value'] 'wildcard' in authorization and authorization['wildcard'] else authorization['identifier']['value']
print("Authorizing {0}".format(authorization['_domain'])) log("Authorizing {0}".format(authorization['_domain']))
# create the challenge # create the challenge
matching_challenges = [c for c in authorization['challenges'] if matching_challenges = [c for c in authorization['challenges'] if
@ -181,7 +183,7 @@ class ACMEAuthority(AbstractACMEAuthority):
# after all challenges are created, start processing authorizations # after all challenges are created, start processing authorizations
for authorization in authorizations: for authorization in authorizations:
print("Starting verification of {}".format(authorization['_domain'])) log("Starting verification of {}".format(authorization['_domain']))
challenge_handlers[authorization['_domain']].start_challenge(authorization['identifier']['value'], challenge_handlers[authorization['_domain']].start_challenge(authorization['identifier']['value'],
account_thumbprint, account_thumbprint,
authorization['_token']) authorization['_token'])
@ -196,7 +198,7 @@ class ACMEAuthority(AbstractACMEAuthority):
code, challenge_status, _ = self._request_url(authorization['_challenge']['url']) code, challenge_status, _ = self._request_url(authorization['_challenge']['url'])
if challenge_status.get('status') == "valid": if challenge_status.get('status') == "valid":
print("{0} verified".format(authorization['_domain'])) log("{0} verified".format(authorization['_domain']))
else: else:
raise ValueError("{0} challenge did not pass: {1}".format( raise ValueError("{0} challenge did not pass: {1}".format(
authorization['_domain'], challenge_status)) authorization['_domain'], challenge_status))
@ -212,7 +214,7 @@ class ACMEAuthority(AbstractACMEAuthority):
challenge_handlers[authorization['_domain']].destroy_challenge( challenge_handlers[authorization['_domain']].destroy_challenge(
authorization['identifier']['value'], account_thumbprint, authorization['_token']) authorization['identifier']['value'], account_thumbprint, authorization['_token'])
except Exception as e: except Exception as e:
print('Challenge destruction failed: {}'.format(e)) log('Challenge destruction failed: {}'.format(e), error=True)
# check order status and retry once # check order status and retry once
code, order, _ = self._request_url(order_url) code, order, _ = self._request_url(order_url)
@ -223,7 +225,7 @@ class ACMEAuthority(AbstractACMEAuthority):
raise ValueError("Order is still not ready to be finalized: {0} {1}".format(code, order)) raise ValueError("Order is still not ready to be finalized: {0} {1}".format(code, order))
# get the new certificate # get the new certificate
print("Finalizing certificate") log("Finalizing certificate")
code, finalize, _ = self._request_acme_url(order['finalize'], { code, finalize, _ = self._request_acme_url(order['finalize'], {
"csr": tools.bytes_to_base64url(tools.convert_cert_to_der_bytes(csr)), "csr": tools.bytes_to_base64url(tools.convert_cert_to_der_bytes(csr)),
}) })
@ -232,7 +234,7 @@ class ACMEAuthority(AbstractACMEAuthority):
code, finalize, _ = self._request_url(order_url) code, finalize, _ = self._request_url(order_url)
if code >= 400: if code >= 400:
raise ValueError("Error finalizing certificate: {0} {1}".format(code, finalize)) raise ValueError("Error finalizing certificate: {0} {1}".format(code, finalize))
print("Certificate ready!") log("Certificate ready!")
# return certificate # return certificate
code, certificate, _ = self._request_url(finalize['certificate'], raw_result=True) code, certificate, _ = self._request_url(finalize['certificate'], raw_result=True)
@ -259,6 +261,6 @@ class ACMEAuthority(AbstractACMEAuthority):
payload['reason'] = int(reason) payload['reason'] = int(reason)
code, result, _ = self._request_acme_endpoint("revokeCert", payload) code, result, _ = self._request_acme_endpoint("revokeCert", payload)
if code < 400: if code < 400:
print("Revocation successful") log("Revocation successful")
else: else:
raise ValueError("Revocation failed: {}".format(result)) raise ValueError("Revocation failed: {}".format(result))

View File

@ -14,6 +14,8 @@ import json
import os import os
import sys import sys
from acertmgr.tools import log
try: try:
import idna import idna
except ImportError: except ImportError:
@ -80,7 +82,7 @@ def idna_convert(domainlist):
return domaintranslation return domaintranslation
else: else:
if 'idna' not in sys.modules: if 'idna' not in sys.modules:
print("Unicode domain found but IDNA names could not be translated due to missing idna module") log("Unicode domain(s) found but IDNA names could not be translated due to missing idna module", error=True)
return list() return list()
@ -143,14 +145,14 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
# SSL cert location (with compatibility to older versions) # SSL cert location (with compatibility to older versions)
if 'server_cert' in globalconfig: if 'server_cert' in globalconfig:
print("WARNING: Legacy configuration directive 'server_cert' used. Support will be removed in 1.0") log("Legacy configuration directive 'server_cert' used. Support will be removed in 1.0", warning=True)
update_config_value(config, 'cert_file', localconfig, globalconfig, update_config_value(config, 'cert_file', localconfig, globalconfig,
globalconfig.get('server_cert', globalconfig.get('server_cert',
os.path.join(config['cert_dir'], "{}.crt".format(config['id'])))) os.path.join(config['cert_dir'], "{}.crt".format(config['id']))))
# SSL key location (with compatibility to older versions) # SSL key location (with compatibility to older versions)
if 'server_key' in globalconfig: if 'server_key' in globalconfig:
print("WARNING: Legacy configuration directive 'server_key' used. Support will be removed in 1.0") log("Legacy configuration directive 'server_key' used. Support will be removed in 1.0", warning=True)
update_config_value(config, 'key_file', localconfig, globalconfig, update_config_value(config, 'key_file', localconfig, globalconfig,
globalconfig.get('server_key', globalconfig.get('server_key',
os.path.join(config['cert_dir'], "{}.key".format(config['id'])))) os.path.join(config['cert_dir'], "{}.key".format(config['id']))))
@ -162,11 +164,13 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
# SSL CA location / use static # SSL CA location / use static
update_config_value(config, 'ca_file', localconfig, globalconfig, update_config_value(config, 'ca_file', localconfig, globalconfig,
globalconfig.get('server_ca', config['defaults'].get('server_ca', globalconfig.get('server_ca', config['defaults'].get('server_ca',
os.path.join(config['cert_dir'], "{}.ca".format(config['id']))))) os.path.join(config['cert_dir'],
"{}.ca".format(
config['id'])))))
update_config_value(config, 'ca_static', localconfig, globalconfig, "false") update_config_value(config, 'ca_static', localconfig, globalconfig, "false")
if 'server_ca' in globalconfig or 'server_ca' in config['defaults']: if 'server_ca' in globalconfig or 'server_ca' in config['defaults']:
config['ca_static'] = "true" config['ca_static'] = "true"
print("WARNING: Legacy configuration directive 'server_ca' used. Support will be removed in 1.0") log("Legacy configuration directive 'server_ca' used. Support removed in 1.0", warning=True)
# Domain action configuration # Domain action configuration
config['actions'] = list() config['actions'] = list()
@ -221,7 +225,8 @@ def load():
if args.config_file: if args.config_file:
global_config_file = args.config_file global_config_file = args.config_file
elif os.path.isfile(LEGACY_CONF_FILE): elif os.path.isfile(LEGACY_CONF_FILE):
print("WARNING: Legacy config file '{}' used. Move to '{}' for 1.0".format(LEGACY_CONF_FILE, DEFAULT_CONF_FILE)) log("Legacy config file '{}' used. Move to '{}' for 1.0".format(LEGACY_CONF_FILE, DEFAULT_CONF_FILE),
warning=True)
global_config_file = LEGACY_CONF_FILE global_config_file = LEGACY_CONF_FILE
else: else:
global_config_file = DEFAULT_CONF_FILE global_config_file = DEFAULT_CONF_FILE
@ -230,7 +235,7 @@ def load():
if args.config_dir: if args.config_dir:
domain_config_dir = args.config_dir domain_config_dir = args.config_dir
elif os.path.isdir(LEGACY_CONF_DIR): elif os.path.isdir(LEGACY_CONF_DIR):
print("WARNING: Legacy config dir '{}' used. Move to '{}' for 1.0".format(LEGACY_CONF_DIR, DEFAULT_CONF_DIR)) log("Legacy config dir '{}' used. Move to '{}' for 1.0".format(LEGACY_CONF_DIR, DEFAULT_CONF_DIR), warning=True)
domain_config_dir = LEGACY_CONF_DIR domain_config_dir = LEGACY_CONF_DIR
else: else:
domain_config_dir = DEFAULT_CONF_DIR domain_config_dir = DEFAULT_CONF_DIR
@ -240,7 +245,7 @@ def load():
if args.work_dir: if args.work_dir:
runtimeconfig['work_dir'] = args.work_dir runtimeconfig['work_dir'] = args.work_dir
elif os.path.isdir(LEGACY_WORK_DIR) and domain_config_dir == LEGACY_CONF_DIR: elif os.path.isdir(LEGACY_WORK_DIR) and domain_config_dir == LEGACY_CONF_DIR:
print("WARNING: Legacy work dir '{}' used. Move to config-dir for 1.0".format(LEGACY_WORK_DIR)) log("Legacy work dir '{}' used. Move to config-dir for 1.0".format(LEGACY_WORK_DIR), warning=True)
runtimeconfig['work_dir'] = LEGACY_WORK_DIR runtimeconfig['work_dir'] = LEGACY_WORK_DIR
else: else:
runtimeconfig['work_dir'] = domain_config_dir runtimeconfig['work_dir'] = domain_config_dir

View File

@ -18,6 +18,7 @@ import dns.update
from acertmgr import tools from acertmgr import tools
from acertmgr.modes.abstract import AbstractChallengeHandler from acertmgr.modes.abstract import AbstractChallengeHandler
from acertmgr.tools import log
REGEX_IP4 = r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$' REGEX_IP4 = r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$'
REGEX_IP6 = r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}' \ REGEX_IP6 = r'^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}' \
@ -187,7 +188,7 @@ class DNSChallengeHandler(AbstractChallengeHandler):
if self.verify_dns_record(domain, txtvalue): if self.verify_dns_record(domain, txtvalue):
return return
else: else:
print("Waiting until TXT record '{}' is ready".format(domain)) log("Waiting until TXT record '{}' is ready".format(domain))
while failtime > datetime.now(): while failtime > datetime.now():
time.sleep(self.dns_verify_interval) time.sleep(self.dns_verify_interval)
if self.verify_dns_record(domain, txtvalue): if self.verify_dns_record(domain, txtvalue):
@ -204,7 +205,7 @@ class DNSChallengeHandler(AbstractChallengeHandler):
ns_ip = self._lookup_ns_ip(domain, nameserverip) ns_ip = self._lookup_ns_ip(domain, nameserverip)
if len(ns_ip) > 0 and all(self._check_txt_record_value(domain, txtvalue, ip) for ip in ns_ip): if len(ns_ip) > 0 and all(self._check_txt_record_value(domain, txtvalue, ip) for ip in ns_ip):
# All NS servers have the necessary TXT record. Succeed immediately! # All NS servers have the necessary TXT record. Succeed immediately!
print("All NS ({}) for '{}' have the correct TXT record".format(','.join(ns_ip), domain)) log("All NS ({}) for '{}' have the correct TXT record".format(','.join(ns_ip), domain))
return True return True
except (ValueError, dns.exception.DNSException): except (ValueError, dns.exception.DNSException):
# Fall back to next verification # Fall back to next verification
@ -216,7 +217,7 @@ class DNSChallengeHandler(AbstractChallengeHandler):
nameserverip = self._lookup_ip(self.dns_verify_server) nameserverip = self._lookup_ip(self.dns_verify_server)
if self._check_txt_record_value(domain, txtvalue, nameserverip): if self._check_txt_record_value(domain, txtvalue, nameserverip):
# Verify server confirms the necessary TXT record. Succeed immediately! # Verify server confirms the necessary TXT record. Succeed immediately!
print("DNS server '{}' found correct TXT record for '{}'".format(self.dns_verify_server, domain)) log("DNS server '{}' found correct TXT record for '{}'".format(self.dns_verify_server, domain))
return True return True
except (ValueError, dns.exception.DNSException): except (ValueError, dns.exception.DNSException):
# Fall back to next verification # Fall back to next verification

View File

@ -13,6 +13,7 @@ import dns.tsigkeyring
import dns.update import dns.update
from acertmgr.modes.dns.abstract import DNSChallengeHandler from acertmgr.modes.dns.abstract import DNSChallengeHandler
from acertmgr.tools import log
DEFAULT_KEY_ALGORITHM = "HMAC-MD5.SIG-ALG.REG.INT" DEFAULT_KEY_ALGORITHM = "HMAC-MD5.SIG-ALG.REG.INT"
@ -29,12 +30,9 @@ class ChallengeHandler(DNSChallengeHandler):
algorithm = re.search(r"algorithm ([a-zA-Z0-9_-]+?);", key_data, re.DOTALL).group(1) algorithm = re.search(r"algorithm ([a-zA-Z0-9_-]+?);", key_data, re.DOTALL).group(1)
tsig_secret = re.search(r"secret \"(.*?)\"", key_data, re.DOTALL).group(1) tsig_secret = re.search(r"secret \"(.*?)\"", key_data, re.DOTALL).group(1)
except IOError as exc: except IOError as exc:
print(exc) raise ValueError("A problem was encountered opening your keyfile '{}': {}".format(tsig_key_file, exc))
raise Exception(
"A problem was encountered opening your keyfile, %s." % tsig_key_file)
except AttributeError as exc: except AttributeError as exc:
print(exc) raise ValueError("Unable to decipher data from your keyfile: {}".format(exc))
raise Exception("Unable to decipher the keyname and secret from your keyfile.")
keyring = dns.tsigkeyring.from_text({ keyring = dns.tsigkeyring.from_text({
key_name: tsig_secret key_name: tsig_secret
@ -73,14 +71,14 @@ class ChallengeHandler(DNSChallengeHandler):
zone, nameserverip = self._determine_zone_and_nameserverip(domain) zone, nameserverip = self._determine_zone_and_nameserverip(domain)
update = dns.update.Update(zone, keyring=self.keyring, keyalgorithm=self.keyalgorithm) update = dns.update.Update(zone, keyring=self.keyring, keyalgorithm=self.keyalgorithm)
update.add(domain, self.dns_ttl, dns.rdatatype.TXT, txtvalue) update.add(domain, self.dns_ttl, dns.rdatatype.TXT, txtvalue)
print('Adding \'{} {} IN TXT "{}"\' to {}'.format(domain, self.dns_ttl, txtvalue, nameserverip)) log('Adding \'{} {} IN TXT "{}"\' to {}'.format(domain, self.dns_ttl, txtvalue, nameserverip))
dns.query.tcp(update, nameserverip) dns.query.tcp(update, nameserverip)
def remove_dns_record(self, domain, txtvalue): def remove_dns_record(self, domain, txtvalue):
zone, nameserverip = self._determine_zone_and_nameserverip(domain) zone, nameserverip = self._determine_zone_and_nameserverip(domain)
update = dns.update.Update(zone, keyring=self.keyring, keyalgorithm=self.keyalgorithm) update = dns.update.Update(zone, keyring=self.keyring, keyalgorithm=self.keyalgorithm)
update.delete(domain, dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, txtvalue)) update.delete(domain, dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, txtvalue))
print('Deleting \'{} {} IN TXT "{}"\' from {}'.format(domain, self.dns_ttl, txtvalue, nameserverip)) log('Deleting \'{} {} IN TXT "{}"\' from {}'.format(domain, self.dns_ttl, txtvalue, nameserverip))
dns.query.tcp(update, nameserverip) dns.query.tcp(update, nameserverip)
def verify_dns_record(self, domain, txtvalue): def verify_dns_record(self, domain, txtvalue):
@ -88,7 +86,7 @@ class ChallengeHandler(DNSChallengeHandler):
# Verify master DNS only if we don't do a full NS check and it has not yet been verified # Verify master DNS only if we don't do a full NS check and it has not yet been verified
_, nameserverip = self._determine_zone_and_nameserverip(domain) _, nameserverip = self._determine_zone_and_nameserverip(domain)
if self._check_txt_record_value(domain, txtvalue, nameserverip, use_tcp=True): if self._check_txt_record_value(domain, txtvalue, nameserverip, use_tcp=True):
print('Verified \'{} {} IN TXT "{}"\' on {}'.format(domain, self.dns_ttl, txtvalue, nameserverip)) log('Verified \'{} {} IN TXT "{}"\' on {}'.format(domain, self.dns_ttl, txtvalue, nameserverip))
self.nsupdate_verified = True self.nsupdate_verified = True
else: else:
# Master DNS verification failed. Return immediately and try again. # Master DNS verification failed. Return immediately and try again.

View File

@ -16,6 +16,7 @@ import socket
import threading import threading
from acertmgr.modes.webdir import HTTPChallengeHandler from acertmgr.modes.webdir import HTTPChallengeHandler
from acertmgr.tools import log
HTTPServer.allow_reuse_address = True HTTPServer.allow_reuse_address = True
@ -36,7 +37,7 @@ class ChallengeHandler(HTTPChallengeHandler):
# Custom HTTP request handler # Custom HTTP request handler
class _HTTPRequestHandler(BaseHTTPRequestHandler): class _HTTPRequestHandler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args): def log_message(self, fmt, *args):
print("Request from '%s': %s" % (self.address_string(), fmt % args)) log("Request from '%s': %s" % (self.address_string(), fmt % args))
def do_GET(self): def do_GET(self):
# Match token on http://<domain>/.well-known/acme-challenge/<token> # Match token on http://<domain>/.well-known/acme-challenge/<token>

View File

@ -11,6 +11,8 @@ import binascii
import datetime import datetime
import io import io
import os import os
import sys
import traceback
import six import six
from cryptography import x509 from cryptography import x509
@ -29,6 +31,35 @@ class InvalidCertificateError(Exception):
pass pass
# @brief wrapper for log output
def log(msg, exc=None, error=False, warning=False):
if error:
prefix = "Error: "
elif warning:
prefix = "Warning: "
else:
prefix = ""
output = prefix + msg
if exc:
_, exc_value, _ = sys.exc_info()
if not getattr(exc, '__traceback__', None) and exc == exc_value:
# Traceback handling on Python 2 is ugly, so we only output it if the exception is the current sys one
formatted_exc = traceback.format_exc()
else:
formatted_exc = traceback.format_exception(type(exc), exc, getattr(exc, '__traceback__', None))
exc_string = ''.join(formatted_exc) if isinstance(formatted_exc, list) else str(formatted_exc)
indent = ' ' * len(prefix)
output += os.linesep + os.linesep.join(indent + line for line in exc_string.splitlines())
if error or warning:
sys.stderr.write(output + os.linesep)
sys.stderr.flush() # force flush buffers after message was written for immediate display
else:
sys.stdout.write(output + os.linesep)
sys.stdout.flush() # force flush buffers after message was written for immediate display
# @brief wrapper for downloading an url # @brief wrapper for downloading an url
def get_url(url, data=None, headers=None): def get_url(url, data=None, headers=None):
return urlopen(Request(url, data=data, headers={} if headers is None else headers)) return urlopen(Request(url, data=data, headers={} if headers is None else headers))
@ -71,7 +102,7 @@ def new_cert_request(names, key, must_staple=False):
if getattr(x509, 'TLSFeature', None): if getattr(x509, 'TLSFeature', None):
req = req.add_extension(x509.TLSFeature(features=[x509.TLSFeatureType.status_request]), critical=False) req = req.add_extension(x509.TLSFeature(features=[x509.TLSFeatureType.status_request]), critical=False)
else: else:
print('OCSP must-staple ignored as current version of cryptography does not support the flag.') log('OCSP must-staple ignored as current version of cryptography does not support the flag.', warning=True)
req = req.sign(key, hashes.SHA256(), default_backend()) req = req.sign(key, hashes.SHA256(), default_backend())
return req return req
@ -101,7 +132,7 @@ def new_ssl_key(path=None, key_size=4096):
try: try:
os.chmod(path, int("0400", 8)) os.chmod(path, int("0400", 8))
except OSError: except OSError:
print('Warning: Could not set file permissions on {0}!'.format(path)) log('Could not set file permissions on {0}!'.format(path), warning=True)
return private_key return private_key
@ -128,7 +159,7 @@ def write_pem_file(crt, path, perms=None):
try: try:
os.chmod(path, perms) os.chmod(path, perms)
except OSError: except OSError:
print('Warning: Could not set file permissions ({0}) on {1}!'.format(perms, path)) log('Could not set file permissions ({0}) on {1}!'.format(perms, path), warning=True)
# @brief download the issuer ca for a given certificate # @brief download the issuer ca for a given certificate
@ -143,14 +174,14 @@ def download_issuer_ca(cert):
break break
if not ca_issuers: if not ca_issuers:
print("Could not determine issuer CA for given certificate: {}".format(cert)) log("Could not determine issuer CA for given certificate: {}".format(cert), error=True)
return None return None
print("Downloading CA certificate from {}".format(ca_issuers)) log("Downloading CA certificate from {}".format(ca_issuers))
resp = get_url(ca_issuers) resp = get_url(ca_issuers)
code = resp.getcode() code = resp.getcode()
if code >= 400: if code >= 400:
print("Could not download issuer CA (error {}) for given certificate: {}".format(code, cert)) log("Could not download issuer CA (error {}) for given certificate: {}".format(code, cert), error=True)
return None return None
return x509.load_der_x509_certificate(resp.read(), default_backend()) return x509.load_der_x509_certificate(resp.read(), default_backend())
@ -159,7 +190,7 @@ def download_issuer_ca(cert):
# @brief determine all san domains on a given certificate # @brief determine all san domains on a given certificate
def get_cert_domains(cert): def get_cert_domains(cert):
if cert is None: if cert is None:
print("WARN: None-certificate has no domains. You have found a bug. Congratulations!") log("None-certificate has no domains. You have found a bug. Congratulations!", warning=True)
return [] return []
san_cert = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME) san_cert = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
@ -169,6 +200,16 @@ def get_cert_domains(cert):
return [cert.subject.rfc4514_string()[3:], ] # strip CN= from the result and return as 1 item list return [cert.subject.rfc4514_string()[3:], ] # strip CN= from the result and return as 1 item list
# @brief determine certificate cn
def get_cert_cn(cert):
return "CN={}".format(cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value)
# @brief determine certificate end of validity
def get_cert_valid_until(cert):
return cert.not_valid_after
# @brief convert certificate to PEM format # @brief convert certificate to PEM format
# @param cert certificate object in pyopenssl format # @param cert certificate object in pyopenssl format
# @return the certificate in PEM format # @return the certificate in PEM format