def _call_freetls(self, vhost): acme_server = self._get_acme_server() logger.info( "Calling issue_certificate for %s on %s", vhost, acme_server) try: freetls.issue_certificate( domains=vhost.domains, certificate_file=vhost.certificate_file, private_key_file=vhost.private_key_file, account_cache_directory=vhost.folder, acme_server=acme_server, agree_to_tos_url=self.tos_url) return except freetls.NeedToAgreeToTOS as tos_error: logger.warning("TOS URL changed to %s", tos_error.url) self.tos_url = tos_error.url except freetls.WaitABit as wait_error: logger.warning("Trying again after: %s", wait_error.until_when) t_delta = wait_error.until_when - datetime.datetime.now() seconds_to_wait = max(t_delta.total_seconds(), 0) # Sleep until we should try it again logger.debug("Sleeping for %i seconds", seconds_to_wait) time.sleep(seconds_to_wait) # Try it again self._call_freetls(vhost)
def do_issue(self, domains=domains, validation_method=client.HTTPValidation(port=5002), **kwargs): client.issue_certificate( domains, self.account_dir, validation_method=validation_method, certificate_file=os.path.join(self.output_dir, "certificate.crt"), certificate_chain_file=os.path.join(self.output_dir, "chain.crt"), acme_server=ACME_SERVER, **kwargs)
def issue_cert(): letsencrypt_issued = False for vhost in VHost.objects.filter(use_letsencrypt=True): if vhost.letsencrypt_state() not in ['REQUEST', 'RENEW']: continue try: data = client.issue_certificate( [str(vhost),] + list(vhost.vhostalias_set.values_list('alias', flat=True)), settings.LETSENCRYPT_STATE_FOLDER, agree_to_tos_url = settings.LETSENCRYPT_TOS, acme_server = settings.LETSENCRYPT_ACME_SERVER) chain = b"\n".join(data['chain']) cert = SSLCert() cert.set_cert(cert=data['cert'].decode(), key=data['private_key'].decode(), ca=chain.decode()) cert.save() vhost.cert = cert vhost.save() vhost.letsencrypt.last_message = '' vhost.letsencrypt.save() letsencrypt_issued = True except client.NeedToTakeAction as e: vhost.letsencrypt.last_message = str(e) for action in e.actions: if isinstance(action, client.NeedToInstallFile): file_name = re.sub(r'[^\w-]', '', action.file_name) with open(settings.LETSENCRYPT_ACME_FOLDER + '/' + file_name, 'w') as f: f.write(action.contents) vhost.letsencrypt.last_message = '' vhost.letsencrypt.save() except Exception as e: vhost.letsencrypt.last_message = str(e) vhost.letsencrypt.save() if letsencrypt_issued and settings.KUMQUAT_USE_0RPC: zerorpc.Client(connect_to=settings.KUMQUAT_BACKEND_SOCKET).update_vhosts()
def provision_certificates(env, agree_to_tos_url=None, logger=None, show_extended_problems=True, force_domains=None, jsonable=False): import requests.exceptions import acme.messages from free_tls_certificates import client # What domains should we provision certificates for? And what # errors prevent provisioning for other domains. domains, problems = get_certificates_to_provision( env, force_domains=force_domains, show_extended_problems=show_extended_problems) # Exit fast if there is nothing to do. if len(domains) == 0: return { "requests": [], "problems": problems, } # Break into groups of up to 100 certificates at a time, which is Let's Encrypt's # limit for a single certificate. We'll sort to put related domains together. domains = sort_domains(domains, env) certs = [] while len(domains) > 0: certs.append(domains[0:100]) domains = domains[100:] # Prepare to provision. # Where should we put our Let's Encrypt account info and state cache. account_path = os.path.join(env['STORAGE_ROOT'], 'ssl/lets_encrypt') if not os.path.exists(account_path): os.mkdir(account_path) # Where should we put ACME challenge files. This is mapped to /.well-known/acme_challenge # by the nginx configuration. challenges_path = os.path.join(account_path, 'acme_challenges') if not os.path.exists(challenges_path): os.mkdir(challenges_path) # Read in the private key that we use for all TLS certificates. We'll need that # to generate a CSR (done by free_tls_certificates). with open(os.path.join(env['STORAGE_ROOT'], 'ssl/ssl_private_key.pem'), 'rb') as f: private_key = f.read() # Provision certificates. ret = [] for domain_list in certs: # For return. ret_item = { "domains": domain_list, "log": [], } ret.append(ret_item) # Logging for free_tls_certificates. def my_logger(message): if logger: logger(message) ret_item["log"].append(message) # Attempt to provision a certificate. try: try: cert = client.issue_certificate( domain_list, account_path, agree_to_tos_url=agree_to_tos_url, private_key=private_key, logger=my_logger) except client.NeedToTakeAction as e: # Write out the ACME challenge files. for action in e.actions: if isinstance(action, client.NeedToInstallFile): fn = os.path.join(challenges_path, action.file_name) with open(fn, 'w') as f: f.write(action.contents) else: raise ValueError(str(action)) # Try to provision now that the challenge files are installed. cert = client.issue_certificate(domain_list, account_path, private_key=private_key, logger=my_logger) except client.NeedToAgreeToTOS as e: # The user must agree to the Let's Encrypt terms of service agreement # before any further action can be taken. ret_item.update({ "result": "agree-to-tos", "url": e.url, }) except client.WaitABit as e: # We need to hold on for a bit before querying again to see if we can # acquire a provisioned certificate. import time, datetime ret_item.update({ "result": "wait", "until": e.until_when if not jsonable else e.until_when.isoformat(), "seconds": (e.until_when - datetime.datetime.now()).total_seconds() }) except client.AccountDataIsCorrupt as e: # This is an extremely rare condition. ret_item.update({ "result": "error", "message": "Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file " + e.account_file_path + ".", }) except (client.InvalidDomainName, client.NeedToTakeAction, client.ChallengeFailed, client.RateLimited, acme.messages.Error, requests.exceptions.RequestException) as e: ret_item.update({ "result": "error", "message": "Something unexpected went wrong: " + str(e), }) else: # A certificate was issued. install_status = install_cert(domain_list[0], cert['cert'].decode("ascii"), b"\n".join( cert['chain']).decode("ascii"), env, raw=True) # str indicates the certificate was not installed. if isinstance(install_status, str): ret_item.update({ "result": "error", "message": "Something unexpected was wrong with the provisioned certificate: " + install_status, }) else: # A list indicates success and what happened next. ret_item["log"].extend(install_status) ret_item.update({ "result": "installed", }) # Return what happened with each certificate request. return { "requests": ret, "problems": problems, }
def provision_certificates(env, agree_to_tos_url=None, logger=None, show_extended_problems=True, force_domains=None, jsonable=False): import requests.exceptions import acme.messages from free_tls_certificates import client # What domains should we provision certificates for? And what # errors prevent provisioning for other domains. domains, problems = get_certificates_to_provision(env, force_domains=force_domains, show_extended_problems=show_extended_problems) # Exit fast if there is nothing to do. if len(domains) == 0: return { "requests": [], "problems": problems, } # Break into groups of up to 100 certificates at a time, which is Let's Encrypt's # limit for a single certificate. We'll sort to put related domains together. domains = sort_domains(domains, env) certs = [] while len(domains) > 0: certs.append( domains[0:100] ) domains = domains[100:] # Prepare to provision. # Where should we put our Let's Encrypt account info and state cache. account_path = os.path.join(env['STORAGE_ROOT'], 'ssl/lets_encrypt') if not os.path.exists(account_path): os.mkdir(account_path) # Where should we put ACME challenge files. This is mapped to /.well-known/acme_challenge # by the nginx configuration. challenges_path = os.path.join(account_path, 'acme_challenges') if not os.path.exists(challenges_path): os.mkdir(challenges_path) # Read in the private key that we use for all TLS certificates. We'll need that # to generate a CSR (done by free_tls_certificates). with open(os.path.join(env['STORAGE_ROOT'], 'ssl/ssl_private_key.pem'), 'rb') as f: private_key = f.read() # Provision certificates. ret = [] for domain_list in certs: # For return. ret_item = { "domains": domain_list, "log": [], } ret.append(ret_item) # Logging for free_tls_certificates. def my_logger(message): if logger: logger(message) ret_item["log"].append(message) # Attempt to provision a certificate. try: try: cert = client.issue_certificate( domain_list, account_path, agree_to_tos_url=agree_to_tos_url, private_key=private_key, logger=my_logger) except client.NeedToTakeAction as e: # Write out the ACME challenge files. for action in e.actions: if isinstance(action, client.NeedToInstallFile): fn = os.path.join(challenges_path, action.file_name) with open(fn, 'w') as f: f.write(action.contents) else: raise ValueError(str(action)) # Try to provision now that the challenge files are installed. cert = client.issue_certificate( domain_list, account_path, private_key=private_key, logger=my_logger) except client.NeedToAgreeToTOS as e: # The user must agree to the Let's Encrypt terms of service agreement # before any further action can be taken. ret_item.update({ "result": "agree-to-tos", "url": e.url, }) except client.WaitABit as e: # We need to hold on for a bit before querying again to see if we can # acquire a provisioned certificate. import time, datetime ret_item.update({ "result": "wait", "until": e.until_when if not jsonable else e.until_when.isoformat(), "seconds": (e.until_when - datetime.datetime.now()).total_seconds() }) except client.AccountDataIsCorrupt as e: # This is an extremely rare condition. ret_item.update({ "result": "error", "message": "Something unexpected went wrong. It looks like your local Let's Encrypt account data is corrupted. There was a problem with the file " + e.account_file_path + ".", }) except (client.InvalidDomainName, client.NeedToTakeAction, client.ChallengeFailed, acme.messages.Error, requests.exceptions.RequestException) as e: ret_item.update({ "result": "error", "message": "Something unexpected went wrong: " + str(e), }) else: # A certificate was issued. install_status = install_cert(domain_list[0], cert['cert'].decode("ascii"), b"\n".join(cert['chain']).decode("ascii"), env, raw=True) # str indicates the certificate was not installed. if isinstance(install_status, str): ret_item.update({ "result": "error", "message": "Something unexpected was wrong with the provisioned certificate: " + install_status, }) else: # A list indicates success and what happened next. ret_item["log"].extend(install_status) ret_item.update({ "result": "installed", }) # Return what happened with each certificate request. return { "requests": ret, "problems": problems, }
def _request_acme_certificate(domain, webroot, nthread): nthread.title = "Requesting ACME certificate" signals.emit("certificates", "pre_add", id) domains = [domain] uid = users.get_system("http").uid gid = groups.get_system("ssl-cert").gid if webroot: webroot = os.path.join(webroot, ".well-known", "acme-challenge") acme_dir = config.get("certificates", "acme_dir") cert_dir = os.path.join(acme_dir, "certs", domain) cert_path = os.path.join(cert_dir, "cert.pem") key_path = os.path.join(cert_dir, "privkey.pem") if not os.path.exists(cert_dir): os.makedirs(cert_dir) if not webroot: sites = websites.get() for x in sites: if x.port in [80, 443] and x.domain == domain: webroot = x.add_acme_challenge() break else: webroot = websites.create_acme_dummy(domain) smsg = "Requesting certificate from Let's Encrypt CA..." nthread.update(Notification("info", "Certificates", smsg)) agree_to_tos = None has_written_files = False while True: try: leclient.issue_certificate( domains, acme_dir, acme_server=config.get("certificates", "acme_server"), certificate_file=cert_path, private_key_file=key_path, agree_to_tos_url=agree_to_tos) break except leclient.NeedToAgreeToTOS as e: agree_to_tos = e.url continue except leclient.NeedToTakeAction as e: if not has_written_files: if not os.path.exists(webroot): os.makedirs(webroot) os.chown(webroot, uid, gid) for x in e.actions: fn = os.path.join(webroot, x.file_name) with open(fn, 'w') as f: f.write(x.contents) os.chown(fn, uid, gid) has_written_files = True continue else: raise errors.InvalidConfigError( "Requesting a certificate failed - it doesn't appear your " "requested domain's DNS is pointing to your server, or " "there was a port problem. Please check these things and " "try again.") except leclient.WaitABit as e: while e.until_when > datetime.datetime.now(): until = e.until_when - datetime.datetime.now() until_secs = int(round(until.total_seconds())) + 1 if until_secs > 300: raise errors.InvalidConfigError( "Requesting a certificate failed - LE rate limiting " "detected, for a period of more than five minutes. " "Please try again later." ) nthread.update( Notification( "warning", "Certificates", "LE rate limiting detected." " Will reattempt in {0} seconds".format(until_secs)) ) time.sleep(until_secs) continue except leclient.InvalidDomainName: raise errors.InvalidConfigError( "Requesting a certificate failed - invalid domain name" ) except leclient.RateLimited: raise errors.InvalidConfigError( "Requesting a certificate failed - LE is refusing to issue " "more certificates to you for this domain. Please choose " "another domain or try again another time." ) os.chown(cert_path, -1, gid) os.chown(key_path, -1, gid) os.chmod(cert_path, 0o750) os.chmod(key_path, 0o750) with open(cert_path, "rb") as f: cert = x509.load_pem_x509_certificate(f.read(), default_backend()) with open(key_path, "rb") as f: key = serialization.load_pem_private_key( f.read(), password=None, backend=default_backend() ) sha1 = binascii.hexlify(cert.fingerprint(hashes.SHA1())).decode() md5 = binascii.hexlify(cert.fingerprint(hashes.MD5())).decode() sha1 = ":".join([sha1[i:i+2].upper() for i in range(0, len(sha1), 2)]) md5 = ":".join([md5[i:i+2].upper() for i in range(0, len(md5), 2)]) if isinstance(key.public_key(), rsa.RSAPublicKey): ktype = "RSA" elif isinstance(key.public_key(), dsa.DSAPublicKey): ktype = "DSA" elif isinstance(key.public_key(), ec.EllipticCurvePublicKey): ktype = "EC" else: ktype = "Unknown" ksize = key.key_size c = Certificate(domain, domain, cert_path, key_path, ktype, ksize, [], cert.not_valid_after, sha1, md5, is_acme=True) storage.certificates[c.id] = c with open("/etc/cron.d/arkos-acme-renew", "a") as f: f.write("0 4 * * * systemctl reload nginx\n") fln = ("30 3 * * * free_tls_certificate {0} {1} {2} {3} {4} " ">> /var/log/acme-renew.log\n") f.write(fln.format( " ".join(domains), key_path, cert_path, webroot.split("/.well-known/acme-challenge")[0], acme_dir )) signals.emit("certificates", "post_add", c) msg = "Certificate issued successfully" nthread.complete(Notification("success", "Certificates", msg)) return c
def provision_certificate(opts): from free_tls_certificates import client import requests.exceptions import acme.messages def logger(msg): print(msg) # It takes multiple invokations of client.issue_certificate to get the job done. agree_to_tos_url = None has_installed_files = False while True: try: # Issue request. client.issue_certificate(opts["domains"], opts["acme_account_path"], certificate_file=opts["certificate_fn"], private_key_file=opts["private_key_fn"], agree_to_tos_url=agree_to_tos_url, acme_server=opts["acme_server"], self_signed=opts["self_signed"], logger=logger) # A certificate was provisioned! return ########################################################################################### except client.AccountDataIsCorrupt as e: # This is an extremely rare condition. print("The account data stored in " + e.account_file_path + " is corrupt.") print("You should probably delete this file and start over.") sys.exit(1) ########################################################################################### except client.NeedToAgreeToTOS as e: # Can't ask user a question interactively if device is not a TTY. if not sys.stdin.isatty(): sys.stderr.write( "You must agree to the Let's Encrypt TOS but input is not a TTY.\n" ) sys.exit(2) sys.stdout.write("""Please open this document in your web browser: %s It is Let's Encrypt's terms of service agreement. If you agree, I can provision your TLS certificate. If you don't agree, this program stops. Do you agree to the agreement? Type Y or N and press <ENTER>: """ % e.url) sys.stdout.flush() if sys.stdin.readline().strip().upper() != "Y": print("\nYou didn't agree. Quitting.") sys.exit(1) # Okay, indicate agreement on next iteration. agree_to_tos_url = e.url # Try again. continue ########################################################################################### except client.InvalidDomainName as e: # One of the domain names provided is not a domain name the ACME # server can issue a certificate for. print(e) sys.exit(1) ########################################################################################### except client.NeedToTakeAction as e: for action in e.actions: if not isinstance(action, client.NeedToInstallFile): raise Exception() fn = os.path.join(opts['static_path'], action.file_name) # Prevent infinite looping. if has_installed_files: print(""" A domain validation challenge file was installed but we couldn't see it on the second pass. That usually means that the domain name does not resolve to the machine this program is running on, or the web server is not serving the static path you specified. Make sure %s is serving the file at %s.""" % (action.url, fn)) sys.exit(1) print("Install domain validation challenge file at " + action.url) # Ensure the static path exists. try: os.makedirs(opts['static_path']) except OSError: # directory already exists pass # Write file. with open(fn, 'w') as f: f.write(action.contents) # Try again. has_installed_files = True continue ########################################################################################### except client.WaitABit as e: # ACME server tells us to try again in a bit. while e.until_when > datetime.datetime.now(): print( "We have to wait %d more seconds for the certificate to be issued..." % int( round((e.until_when - datetime.datetime.now()).total_seconds()))) time.sleep(15) # Try again. continue ########################################################################################### except client.RateLimited as e: # The ACME server is refusing to issue more certificates for a second-level domain # for your account. print(e) sys.exit(1) ########################################################################################### except acme.messages.Error as e: # A protocol error occurred. (If a CSR was supplied, it might # be for a different set of domains than was specified, for instance.) print("Something went wrong: " + str(e)) sys.exit(1) ########################################################################################### except requests.exceptions.RequestException as e: # A DNS or network error occurred. print("Something went wrong:" + str(e)) sys.exit(1)
def provision_certificate(opts): from free_tls_certificates import client import requests.exceptions import acme.messages def logger(msg): print(msg) # It takes multiple invokations of client.issue_certificate to get the job done. agree_to_tos_url = None has_installed_files = False while True: try: # Issue request. client.issue_certificate( opts["domains"], opts["acme_account_path"], certificate_file=opts["certificate_fn"], private_key_file=opts["private_key_fn"], agree_to_tos_url=agree_to_tos_url, acme_server=opts["acme_server"], self_signed=opts["self_signed"], logger=logger) # A certificate was provisioned! return ########################################################################################### except client.AccountDataIsCorrupt as e: # This is an extremely rare condition. print("The account data stored in " + e.account_file_path + " is corrupt.") print("You should probably delete this file and start over.") sys.exit(1) ########################################################################################### except client.NeedToAgreeToTOS as e: # Can't ask user a question interactively if device is not a TTY. if not sys.stdin.isatty(): sys.stderr.write("You must agree to the Let's Encrypt TOS but input is not a TTY.\n") sys.exit(2) sys.stdout.write("""Please open this document in your web browser: %s It is Let's Encrypt's terms of service agreement. If you agree, I can provision your TLS certificate. If you don't agree, this program stops. Do you agree to the agreement? Type Y or N and press <ENTER>: """ % e.url) sys.stdout.flush() if sys.stdin.readline().strip().upper() != "Y": print("\nYou didn't agree. Quitting.") sys.exit(1) # Okay, indicate agreement on next iteration. agree_to_tos_url = e.url # Try again. continue ########################################################################################### except client.InvalidDomainName as e: # One of the domain names provided is not a domain name the ACME # server can issue a certificate for. print(e) sys.exit(1) ########################################################################################### except client.NeedToTakeAction as e: for action in e.actions: if not isinstance(action, client.NeedToInstallFile): raise Exception() fn = os.path.join(opts['static_path'], action.file_name) # Prevent infinite looping. if has_installed_files: print(""" A domain validation challenge file was installed but we couldn't see it on the second pass. That usually means that the domain name does not resolve to the machine this program is running on, or the web server is not serving the static path you specified. Make sure %s is serving the file at %s.""" % (action.url, fn)) sys.exit(1) print("Install domain validation challenge file at " + action.url) # Ensure the static path exists. try: os.makedirs(opts['static_path']) except OSError: # directory already exists pass # Write file. with open(fn, 'w') as f: f.write(action.contents) # Try again. has_installed_files = True continue ########################################################################################### except client.WaitABit as e: # ACME server tells us to try again in a bit. while e.until_when > datetime.datetime.now(): print ("We have to wait %d more seconds for the certificate to be issued..." % int(round((e.until_when - datetime.datetime.now()).total_seconds()))) time.sleep(15) # Try again. continue ########################################################################################### except client.RateLimited as e: # The ACME server is refusing to issue more certificates for a second-level domain # for your account. print(e) sys.exit(1) ########################################################################################### except acme.messages.Error as e: # A protocol error occurred. (If a CSR was supplied, it might # be for a different set of domains than was specified, for instance.) print("Something went wrong: " + str(e)) sys.exit(1) ########################################################################################### except requests.exceptions.RequestException as e: # A DNS or network error occurred. print("Something went wrong:" + str(e)) sys.exit(1)