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.authority import authority
from acertmgr.modes import challenge_handler
from acertmgr.tools import log
# @brief fetch new certificate from letsencrypt
# @param settings the domain's configuration options
def cert_get(settings):
print("Getting certificate for %s" % settings['domainlist'])
log("Getting certificate for %s" % settings['domainlist'])
acme = authority(settings['authority'])
acme.register_account()
@ -38,16 +39,16 @@ def cert_get(settings):
if os.path.isfile(key_file):
key = tools.read_pem_file(key_file, key=True)
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)
# create ssl csr
csr_file = settings['csr_file']
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)
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"
cr = tools.new_cert_request(settings['domainlist'], key, must_staple)
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 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)
if (not str(settings.get('ca_static')).lower() == 'true' or not os.path.exists(settings['ca_file'])) \
and ca is not None:
@ -107,11 +109,11 @@ def cert_put(settings):
try:
os.chown(crt_path, uid, gid)
except OSError:
print('Warning: Could not set certificate file ownership!')
log('Could not set certificate file ownership!', warning=True)
try:
os.chmod(crt_path, int(crt_perm, 8))
except OSError:
print('Warning: Could not set certificate file permissions!')
log('Could not set certificate file permissions!', warning=True)
return crt_action
@ -131,7 +133,7 @@ def main():
runtimeconfig, domainconfigs = configuration.load()
if runtimeconfig.get('mode') == 'revoke':
# 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'])
else:
# Mode: issue certificates (implicit)
@ -152,7 +154,7 @@ def main():
if str(config.get('cert_revoke_superseded')).lower() == 'true' and cert:
superseded.add(cert)
except Exception as e:
print("Certificate issue/renew failed: {}".format(e))
log("Certificate issue/renew failed", e, error=True)
exceptions.append(e)
# deploy new certificates after all are renewed
@ -161,10 +163,10 @@ def main():
try:
for cfg in config['actions']:
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))
except Exception as e:
print("Certificate deployment failed: {}".format(e))
log("Certificate deployment failed", e, error=True)
exceptions.append(e)
deployment_success = False
@ -174,9 +176,10 @@ def main():
try:
# 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)
print("Executed '{}' successfully: {}".format(action, output))
log("Executed '{}' successfully: {}".format(action, output))
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)
deployment_success = False
@ -184,14 +187,14 @@ def main():
if deployment_success:
for superseded_cert in superseded:
try:
print("Revoking previous certificate '{}' valid until {} as superseded".format(
superseded_cert,
superseded_cert.not_valid_after))
log("Revoking '{}' valid until {} as superseded".format(
tools.get_cert_cn(superseded_cert),
tools.get_cert_valid_until(superseded_cert)))
cert_revoke(superseded_cert, domainconfigs, reason=4) # reason=4 is superseded
except Exception as e:
print("Certificate supersede revoke failed: {}".format(e))
log("Certificate supersede revoke failed", e, error=True)
exceptions.append(e)
# throw a RuntimeError with all exceptions caught while working if there were any
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
from acertmgr import tools
from acertmgr.tools import log
authorities = dict()
@ -23,10 +24,10 @@ def authority(settings):
else:
acc_file = settings['account_key']
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)
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)
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.authority.acme import ACMEAuthority as AbstractACMEAuthority
from acertmgr.tools import log
class ACMEAuthority(AbstractACMEAuthority):
@ -70,11 +71,11 @@ class ACMEAuthority(AbstractACMEAuthority):
"agreement": self.agreement,
})
if code == 201:
print("Registered!")
log("Registered!")
self.registered_account = True
return True
elif code == 409:
print("Already registered!")
log("Already registered!")
self.registered_account = True
return False
else:
@ -96,7 +97,7 @@ class ACMEAuthority(AbstractACMEAuthority):
# verify each domain
try:
for domain in domains:
print("Verifying {0}...".format(domain))
log("Verifying {0}...".format(domain))
# get new challenge
code, result = self._send_signed(self.ca + "/acme/new-authz", header, {
@ -120,7 +121,7 @@ class ACMEAuthority(AbstractACMEAuthority):
for domain in domains:
challenge_handlers[domain].start_challenge(domain, account_thumbprint, tokens[domain])
try:
print("Starting key authorization")
log("Starting key authorization")
# notify challenge are met
keyauthorization = "{0}.{1}".format(tokens[domain], account_thumbprint)
code, result = self._send_signed(challenges[domain]['uri'], header, {
@ -141,7 +142,7 @@ class ACMEAuthority(AbstractACMEAuthority):
if challenge_status['status'] == "pending":
time.sleep(2)
elif challenge_status['status'] == "valid":
print("{0} verified!".format(domain))
log("{0} verified!".format(domain))
break
else:
raise ValueError("{0} challenge did not pass: {1}".format(
@ -155,10 +156,10 @@ class ACMEAuthority(AbstractACMEAuthority):
try:
challenge_handlers[domain].destroy_challenge(domain, account_thumbprint, tokens[domain])
except Exception as e:
print('Challenge destruction failed: {}'.format(e))
log('Challenge destruction failed: {}'.format(e), error=True)
# get the new certificate
print("Signing certificate...")
log("Signing certificate...")
code, result = self._send_signed(self.ca + "/acme/new-cert", header, {
"resource": "new-cert",
"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))
# return signed certificate!
print("Certificate signed!")
log("Certificate signed!")
cert = tools.convert_der_bytes_to_cert(result)
return cert, tools.download_issuer_ca(cert)
@ -181,6 +182,6 @@ class ACMEAuthority(AbstractACMEAuthority):
payload['reason'] = int(reason)
code, result = self._send_signed(self.ca + "/acme/revoke-cert", header, payload)
if code < 400:
print("Revocation successful")
log("Revocation successful")
else:
raise ValueError("Revocation failed: {}".format(result))

View File

@ -12,6 +12,7 @@ import time
from acertmgr import tools
from acertmgr.authority.acme import ACMEAuthority as AbstractACMEAuthority
from acertmgr.tools import log
class ACMEAuthority(AbstractACMEAuthority):
@ -41,7 +42,8 @@ class ACMEAuthority(AbstractACMEAuthority):
"newOrder": "{}/acme/new-order".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.algorithm, jwk = tools.get_key_alg_and_jwk(key)
@ -127,8 +129,8 @@ class ACMEAuthority(AbstractACMEAuthority):
if code < 400 and result['status'] == 'valid':
self.account_id = headers['Location']
if 'meta' in self.directory and 'termsOfService' in self.directory['meta']:
print("ToS at {} have been accepted.".format(self.directory['meta']['termsOfService']))
print("Account registered and valid.".format())
log("ToS at {} have been accepted.".format(self.directory['meta']['termsOfService']))
log("Account registered and valid on {}.".format(self.ca))
else:
raise ValueError("Error registering account: {0} {1}".format(code, result))
@ -142,7 +144,7 @@ class ACMEAuthority(AbstractACMEAuthority):
account_thumbprint = tools.bytes_to_base64url(
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]
code, order, headers = self._request_acme_endpoint('newOrder', {'identifiers': identifiers})
if code >= 400:
@ -160,7 +162,7 @@ class ACMEAuthority(AbstractACMEAuthority):
authorization['_domain'] = "*.{}".format(authorization['identifier']['value']) if \
'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
matching_challenges = [c for c in authorization['challenges'] if
@ -181,7 +183,7 @@ class ACMEAuthority(AbstractACMEAuthority):
# after all challenges are created, start processing 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'],
account_thumbprint,
authorization['_token'])
@ -196,7 +198,7 @@ class ACMEAuthority(AbstractACMEAuthority):
code, challenge_status, _ = self._request_url(authorization['_challenge']['url'])
if challenge_status.get('status') == "valid":
print("{0} verified".format(authorization['_domain']))
log("{0} verified".format(authorization['_domain']))
else:
raise ValueError("{0} challenge did not pass: {1}".format(
authorization['_domain'], challenge_status))
@ -212,7 +214,7 @@ class ACMEAuthority(AbstractACMEAuthority):
challenge_handlers[authorization['_domain']].destroy_challenge(
authorization['identifier']['value'], account_thumbprint, authorization['_token'])
except Exception as e:
print('Challenge destruction failed: {}'.format(e))
log('Challenge destruction failed: {}'.format(e), error=True)
# check order status and retry once
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))
# get the new certificate
print("Finalizing certificate")
log("Finalizing certificate")
code, finalize, _ = self._request_acme_url(order['finalize'], {
"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)
if code >= 400:
raise ValueError("Error finalizing certificate: {0} {1}".format(code, finalize))
print("Certificate ready!")
log("Certificate ready!")
# return certificate
code, certificate, _ = self._request_url(finalize['certificate'], raw_result=True)
@ -259,6 +261,6 @@ class ACMEAuthority(AbstractACMEAuthority):
payload['reason'] = int(reason)
code, result, _ = self._request_acme_endpoint("revokeCert", payload)
if code < 400:
print("Revocation successful")
log("Revocation successful")
else:
raise ValueError("Revocation failed: {}".format(result))

View File

@ -14,6 +14,8 @@ import json
import os
import sys
from acertmgr.tools import log
try:
import idna
except ImportError:
@ -80,7 +82,7 @@ def idna_convert(domainlist):
return domaintranslation
else:
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()
@ -143,14 +145,14 @@ def parse_config_entry(entry, globalconfig, runtimeconfig):
# SSL cert location (with compatibility to older versions)
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,
globalconfig.get('server_cert',
os.path.join(config['cert_dir'], "{}.crt".format(config['id']))))
# SSL key location (with compatibility to older versions)
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,
globalconfig.get('server_key',
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
update_config_value(config, 'ca_file', localconfig, globalconfig,
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")
if 'server_ca' in globalconfig or 'server_ca' in config['defaults']:
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
config['actions'] = list()
@ -221,7 +225,8 @@ def load():
if args.config_file:
global_config_file = args.config_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
else:
global_config_file = DEFAULT_CONF_FILE
@ -230,7 +235,7 @@ def load():
if args.config_dir:
domain_config_dir = args.config_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
else:
domain_config_dir = DEFAULT_CONF_DIR
@ -240,7 +245,7 @@ def load():
if args.work_dir:
runtimeconfig['work_dir'] = args.work_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
else:
runtimeconfig['work_dir'] = domain_config_dir

View File

@ -18,6 +18,7 @@ import dns.update
from acertmgr import tools
from acertmgr.modes.abstract import AbstractChallengeHandler
from acertmgr.tools import log
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}' \
@ -187,7 +188,7 @@ class DNSChallengeHandler(AbstractChallengeHandler):
if self.verify_dns_record(domain, txtvalue):
return
else:
print("Waiting until TXT record '{}' is ready".format(domain))
log("Waiting until TXT record '{}' is ready".format(domain))
while failtime > datetime.now():
time.sleep(self.dns_verify_interval)
if self.verify_dns_record(domain, txtvalue):
@ -204,7 +205,7 @@ class DNSChallengeHandler(AbstractChallengeHandler):
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):
# 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
except (ValueError, dns.exception.DNSException):
# Fall back to next verification
@ -216,7 +217,7 @@ class DNSChallengeHandler(AbstractChallengeHandler):
nameserverip = self._lookup_ip(self.dns_verify_server)
if self._check_txt_record_value(domain, txtvalue, nameserverip):
# 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
except (ValueError, dns.exception.DNSException):
# Fall back to next verification

View File

@ -13,6 +13,7 @@ import dns.tsigkeyring
import dns.update
from acertmgr.modes.dns.abstract import DNSChallengeHandler
from acertmgr.tools import log
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)
tsig_secret = re.search(r"secret \"(.*?)\"", key_data, re.DOTALL).group(1)
except IOError as exc:
print(exc)
raise Exception(
"A problem was encountered opening your keyfile, %s." % tsig_key_file)
raise ValueError("A problem was encountered opening your keyfile '{}': {}".format(tsig_key_file, exc))
except AttributeError as exc:
print(exc)
raise Exception("Unable to decipher the keyname and secret from your keyfile.")
raise ValueError("Unable to decipher data from your keyfile: {}".format(exc))
keyring = dns.tsigkeyring.from_text({
key_name: tsig_secret
@ -73,14 +71,14 @@ class ChallengeHandler(DNSChallengeHandler):
zone, nameserverip = self._determine_zone_and_nameserverip(domain)
update = dns.update.Update(zone, keyring=self.keyring, keyalgorithm=self.keyalgorithm)
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)
def remove_dns_record(self, domain, txtvalue):
zone, nameserverip = self._determine_zone_and_nameserverip(domain)
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))
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)
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
_, nameserverip = self._determine_zone_and_nameserverip(domain)
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
else:
# Master DNS verification failed. Return immediately and try again.

View File

@ -16,6 +16,7 @@ import socket
import threading
from acertmgr.modes.webdir import HTTPChallengeHandler
from acertmgr.tools import log
HTTPServer.allow_reuse_address = True
@ -36,7 +37,7 @@ class ChallengeHandler(HTTPChallengeHandler):
# Custom HTTP request handler
class _HTTPRequestHandler(BaseHTTPRequestHandler):
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):
# Match token on http://<domain>/.well-known/acme-challenge/<token>

View File

@ -11,6 +11,8 @@ import binascii
import datetime
import io
import os
import sys
import traceback
import six
from cryptography import x509
@ -29,6 +31,35 @@ class InvalidCertificateError(Exception):
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
def get_url(url, data=None, headers=None):
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):
req = req.add_extension(x509.TLSFeature(features=[x509.TLSFeatureType.status_request]), critical=False)
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())
return req
@ -101,7 +132,7 @@ def new_ssl_key(path=None, key_size=4096):
try:
os.chmod(path, int("0400", 8))
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
@ -128,7 +159,7 @@ def write_pem_file(crt, path, perms=None):
try:
os.chmod(path, perms)
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
@ -143,14 +174,14 @@ def download_issuer_ca(cert):
break
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
print("Downloading CA certificate from {}".format(ca_issuers))
log("Downloading CA certificate from {}".format(ca_issuers))
resp = get_url(ca_issuers)
code = resp.getcode()
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 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
def get_cert_domains(cert):
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 []
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
# @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
# @param cert certificate object in pyopenssl format
# @return the certificate in PEM format