Exemple #1
0
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)
Exemple #2
0
    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)
Exemple #3
0
    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
Exemple #4
0
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)))