def cert_get(settings): print("Getting certificate for '%s'." % settings['domains']) key_file = settings['key_file'] key_length = settings['key_length'] if not os.path.isfile(key_file): print("SSL key not found at '{0}'. Creating {1} bit RSA key.".format( key_file, key_length)) tools.new_rsa_key(key_file, key_length) acme = create_authority(settings) filename = settings['id'] _, csr_file = tempfile.mkstemp(".csr", "%s." % filename) _, crt_file = tempfile.mkstemp(".crt", "%s." % filename) # find challenge handlers for this certificate challenge_handlers = dict() for domain in settings['domainlist']: # Create the challenge handler challenge_handlers[domain] = create_challenge_handler( settings['handlers'][domain]) try: key = tools.read_key(key_file) cr = tools.new_cert_request(settings['domainlist'], key) print("Reading account key...") acme.register_account() crt = acme.get_crt_from_csr(cr, settings['domainlist'], challenge_handlers) with io.open(crt_file, "w") as crt_fd: crt_fd.write(tools.convert_cert_to_pem(crt)) # if resulting certificate is valid: store in final location if tools.is_cert_valid(crt_file, 60): crt_final = settings['cert_file'] shutil.copy2(crt_file, crt_final) os.chmod(crt_final, stat.S_IREAD) # download current ca file for the new certificate if no static ca is configured if "static_ca" in settings and not settings['static_ca']: tools.download_issuer_ca(crt_final, settings['ca_file']) finally: os.remove(csr_file) os.remove(crt_file)
def get_crt_from_csr(self, csr, domains, challenge_handlers): header = self._prepare_header() account_thumbprint = tools.bytes_to_base64url( tools.hash_of_str( json.dumps(header['jwk'], sort_keys=True, separators=(',', ':')))) challenges = dict() tokens = dict() authdomains = list() # verify each domain try: for domain in domains: log("Verifying {0}...".format(domain)) # get new challenge code, result = self._send_signed( self.ca + "/acme/new-authz", header, { "resource": "new-authz", "identifier": { "type": "dns", "value": domain }, }) if code != 201: raise ValueError( "Error requesting challenges: {0} {1}".format( code, result)) # create the challenge authz = json.loads(result.decode('utf8')) if authz.get('status', 'no-status-found') == 'valid': log("{} has already been verified".format(domain)) continue challenges[domain] = [ c for c in authz['challenges'] if c['type'] == challenge_handlers[domain].get_challenge_type() ][0] tokens[domain] = re.sub(r"[^A-Za-z0-9_\-]", "_", challenges[domain]['token']) if domain not in challenge_handlers: raise ValueError( "No challenge handler given for domain: {0}".format( domain)) challenge_handlers[domain].create_challenge( domain, account_thumbprint, tokens[domain]) authdomains.append(domain) # after all challenges are created, start processing authorizations for domain in authdomains: try: challenge_handlers[domain].start_challenge( domain, account_thumbprint, tokens[domain]) # notify challenge are met log("Starting key authorization") keyauthorization = "{0}.{1}".format( tokens[domain], account_thumbprint) code, result = self._send_signed( challenges[domain]['uri'], header, { "resource": "challenge", "keyAuthorization": keyauthorization, }) if code != 202: raise ValueError( "Error triggering challenge: {0} {1}".format( code, result)) # wait for challenge to be verified while True: try: resp = tools.get_url(challenges[domain]['uri']) challenge_status = json.loads( resp.read().decode('utf8')) except IOError as e: raise ValueError( "Error checking challenge: {0} {1}".format( e.code, json.loads(e.read().decode('utf8')))) if challenge_status['status'] == "pending": time.sleep(2) elif challenge_status['status'] == "valid": log("{0} verified!".format(domain)) break else: raise ValueError( "{0} challenge did not pass: {1}".format( domain, challenge_status)) finally: challenge_handlers[domain].stop_challenge( domain, account_thumbprint, tokens[domain]) finally: # Destroy challenge handlers in reverse order to replay # any saved state information in the handlers correctly for domain in reversed(domains): try: challenge_handlers[domain].destroy_challenge( domain, account_thumbprint, tokens[domain]) except Exception as e: log('Challenge destruction failed: {}'.format(e), error=True) # get the new 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)), }) if code != 201: raise ValueError("Error signing certificate: {0} {1}".format( code, result)) # return signed certificate! log("Certificate signed!") cert = tools.convert_der_bytes_to_cert(result) return cert, tools.download_issuer_ca(cert)
def get_crt_from_csr(self, csr, domains, challenge_handlers): account_thumbprint = tools.bytes_to_base64url( tools.hash_of_str( json.dumps(self.account_protected['jwk'], sort_keys=True, separators=(',', ':')))) 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: raise ValueError("Error with certificate order: {0} {1}".format( code, order)) order_url = headers['Location'] authorizations = list() # verify each domain try: for authorizationUrl in order['authorizations']: # get new challenge code, authorization, _ = self._request_acme_url( authorizationUrl) if code >= 400: raise ValueError( "Error requesting authorization: {0} {1}".format( code, authorization)) authorization['_domain'] = "*.{}".format(authorization['identifier']['value']) if \ 'wildcard' in authorization and authorization['wildcard'] else authorization['identifier']['value'] if authorization.get('status', 'no-status-found') == 'valid': log("{} has already been authorized".format( authorization['_domain'])) continue if authorization['_domain'] not in challenge_handlers: raise ValueError( "No challenge handler given for domain: {0}".format( authorization['_domain'])) log("Authorizing {0}".format(authorization['_domain'])) # create the challenge ctype = challenge_handlers[ authorization['_domain']].get_challenge_type() matching_challenges = [ c for c in authorization['challenges'] if c['type'] == ctype ] if len(matching_challenges) == 0: raise ValueError( "Error no challenge matching {0} found: {1}".format( ctype, authorization)) authorization['_challenge'] = matching_challenges[0] if authorization['_challenge'].get( 'status', 'no-status-found') == 'valid': log("{} has already been authorized using {}".format( authorization['_domain'], ctype)) continue authorization['_token'] = re.sub( r"[^A-Za-z0-9_\-]", "_", authorization['_challenge']['token']) challenge_handlers[authorization['_domain']].create_challenge( authorization['identifier']['value'], account_thumbprint, authorization['_token']) authorizations.append(authorization) # after all challenges are created, start processing authorizations for authorization in authorizations: try: log("Starting verification of {}".format( authorization['_domain'])) challenge_handlers[ authorization['_domain']].start_challenge( authorization['identifier']['value'], account_thumbprint, authorization['_token']) # notify challenge is met code, challenge_status, _ = self._request_acme_url( authorization['_challenge']['url'], { "keyAuthorization": "{0}.{1}".format(authorization['_token'], account_thumbprint), }) # wait for challenge to be verified while code < 400 and challenge_status.get( 'status') == "pending": time.sleep(5) code, challenge_status, _ = self._request_acme_url( authorization['_challenge']['url']) if code < 400 and challenge_status.get( 'status') == "valid": log("{0} verified".format(authorization['_domain'])) else: raise ValueError( "{0} challenge did not pass ({1}): {2}".format( authorization['_domain'], code, challenge_status)) finally: challenge_handlers[ authorization['_domain']].stop_challenge( authorization['identifier']['value'], account_thumbprint, authorization['_token']) finally: # Destroy challenge handlers in reverse order to replay # any saved state information in the handlers correctly for authorization in reversed(authorizations): try: challenge_handlers[ authorization['_domain']].destroy_challenge( authorization['identifier']['value'], account_thumbprint, authorization['_token']) except Exception as e: log('Challenge destruction failed: {}'.format(e), error=True) # check order status and retry once code, order, _ = self._request_acme_url(order_url) if code < 400 and order.get('status') == 'pending': time.sleep(5) code, order, _ = self._request_acme_url(order_url) if code >= 400: raise ValueError( "Order is still not ready to be finalized: {0} {1}".format( code, order)) # get the new certificate log("Finalizing certificate") code, finalize, _ = self._request_acme_url( order['finalize'], { "csr": tools.bytes_to_base64url(tools.convert_cert_to_der_bytes(csr)), }) while code < 400 and (finalize.get('status') == 'pending' or finalize.get('status') == 'processing'): time.sleep(5) code, finalize, _ = self._request_acme_url(order_url) if code >= 400: raise ValueError("Error finalizing certificate: {0} {1}".format( code, finalize)) log("Certificate ready!") # return certificate code, certificate, _ = self._request_acme_url(finalize['certificate'], raw_result=True) if code >= 400: raise ValueError( "Error downloading certificate chain: {0} {1}".format( code, certificate)) cert_dict = re.match(( r'(?P<cert>-----BEGIN CERTIFICATE-----[^\-]+-----END CERTIFICATE-----)\n\n' r'(?P<ca>-----BEGIN CERTIFICATE-----[^\-]+-----END CERTIFICATE-----)?' ), certificate, re.DOTALL).groupdict() cert = tools.convert_pem_str_to_cert(cert_dict['cert']) if cert_dict['ca'] is None: ca = tools.download_issuer_ca(cert) else: ca = tools.convert_pem_str_to_cert(cert_dict['ca']) return cert, ca
def main(): # load config runtimeconfig, domainconfigs = configuration.load() # register idna-mapped domains as LOG_REPLACEMENTS for better readability of log output LOG_REPLACEMENTS.update({k: "{} [{}]".format(k, v) for k, v in domainconfigs['domainlist_idna_mapped']}) # Start processing if runtimeconfig.get('mode') == 'revoke': # Mode: revoke certificate log("Revoking {}".format(runtimeconfig['revoke'])) cert_revoke(tools.read_pem_file(runtimeconfig['revoke']), domainconfigs, runtimeconfig['fallback_authority'], runtimeconfig['revoke_reason']) else: # Mode: issue certificates (implicit) # post-update actions (run only once) actions = set() superseded = set() exceptions = list() # check certificate validity and obtain/renew certificates if needed for config in domainconfigs: try: cert = None if os.path.isfile(config['cert_file']): cert = tools.read_pem_file(config['cert_file']) validate_ocsp = str(config.get('validate_ocsp')).lower() != 'false' if validate_ocsp and cert and os.path.isfile(config['ca_file']): try: issuer = tools.read_pem_file(config['ca_file']) except Exception as e1: log("Failed to retrieve issuer from ca file: {}. Trying to download...".format(e1)) try: issuer = tools.download_issuer_ca(cert) except Exception as e2: log("Failed to download issuer for cert file: {}. Cannot validate OCSP.".format(e2)) validate_ocsp = False if not cert or ('force_renew' in runtimeconfig and all( d in config['domainlist'] for d in runtimeconfig['force_renew'])) \ or not tools.is_cert_valid(cert, config['ttl_days']) \ or (validate_ocsp and not tools.is_ocsp_valid(cert, issuer, config['validate_ocsp'])): cert_get(config) if str(config.get('cert_revoke_superseded')).lower() == 'true' and cert: superseded.add(cert) except Exception as e: log("Certificate issue/renew failed", e, error=True) exceptions.append(e) # deploy new certificates after all are renewed deployment_success = True for config in domainconfigs: for cfg in config['actions']: try: if not tools.target_is_current(cfg['path'], config['cert_file']): actions.add(cert_put(cfg)) log("Updated '{}' due to newer version".format(cfg['path'])) except Exception as e: log("Certificate deployment to {} failed".format(cfg['path']), e, error=True) exceptions.append(e) deployment_success = False # run post-update actions for action in actions: if action is not None: 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) logmsg = "Action succeeded: {}".format(action) if len(output) > 0: if getattr(output, 'decode', None): # Decode function available? Use it to get a proper str output = output.decode('utf-8') logmsg += os.linesep + tools.indent(output, 18) # 18 = len("Action succeeded: ") log(logmsg) except subprocess.CalledProcessError as e: output = e.output logmsg = "Action failed: ({}) {}".format(e.returncode, e.cmd) if len(output) > 0: if getattr(output, 'decode', None): # Decode function available? Use it to get a proper str output = output.decode('utf-8') logmsg += os.linesep + tools.indent(output, 15) # 15 = len("Action failed: ") log(logmsg, error=True) exceptions.append(e) deployment_success = False # revoke old certificates as superseded if deployment_success: for superseded_cert in superseded: try: 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, runtimeconfig['fallback_authority'], reason=4) except Exception as 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 processing".format(len(exceptions)))