def _send_signed_request(url, payload): nonce = requests.get(CA + "/directory").headers['Replay-Nonce'] payload64 = _b64(json.dumps(payload)) protected = copy.deepcopy(header) protected.update({"nonce": nonce}) protected64 = _b64(json.dumps(protected)) out = openssl("dgst", "-sha256", "-sign", account_key) data = json.dumps({ "header": header, "protected": protected64, "payload": payload64, "signature": _b64(out), }) resp = requests.get(url, payload=data) return resp.status_code, resp.json()
def get_crt(account_key, csr, acme_dir): from plumbum.cmd import openssl # helper function base64 encode for jose spec def _b64(b): return base64.urlsafe_b64encode(b).replace("=", "") # parse account key to get public key info("Parsing account key...") out = openssl("rsa", "-in", account_key, "-noout", "-text") pub_hex, pub_exp = re.search( r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", out, re.MULTILINE | re.DOTALL).groups() pub_mod = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex)) pub_mod64 = _b64(pub_mod) pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{}".format(pub_exp) if len(pub_exp) % 2 else pub_exp pub_exp64 = _b64(binascii.unhexlify(pub_exp)) header = { "alg": "RS256", "jwk": { "e": pub_exp64, "kty": "RSA", "n": pub_mod64, }, } accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':')) thumbprint = _b64(hashlib.sha256(accountkey_json).digest()) info("parsed!\n") # helper function make signed requests def _send_signed_request(url, payload): nonce = requests.get(CA + "/directory").headers['Replay-Nonce'] payload64 = _b64(json.dumps(payload)) protected = copy.deepcopy(header) protected.update({"nonce": nonce}) protected64 = _b64(json.dumps(protected)) out = openssl("dgst", "-sha256", "-sign", account_key) data = json.dumps({ "header": header, "protected": protected64, "payload": payload64, "signature": _b64(out), }) resp = requests.get(url, payload=data) return resp.status_code, resp.json() # find domains info("Parsing CSR...") out = openssl("req", "-in", csr, "-noout", "-text") domains = set([]) common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out) if common_name is not None: domains.add(common_name.group(1)) subject_alt_names = re.search( "X509v3 Subject Alternative Name: \n +([^\n]+)\n", out, re.MULTILINE | re.DOTALL) if subject_alt_names is not None: for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): domains.add(san[4:]) info("parsed!\n") # get the certificate domains and expiration info("Registering account...") code, result = _send_signed_request(CA + "/acme/new-reg", { "resource": "new-reg", "agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf", }) if code == 201: info("registered!\n") elif code == 409: warn("already registered!\n") else: raise ValueError("Error registering: {} {}".format(code, result)) # verify each domain for domain in domains: info("Verifying {}...", domain) # get new challenge code, result = _send_signed_request(CA + "/acme/new-authz", { "resource": "new-authz", "identifier": { "type": "dns", "value": domain, }, }) if code != 201: raise ValueError("Error registering: {} {}".format(code, result)) # make the challenge file challenge = [c for c in result['challenges'] if c['type'] == "http-01"][0] keyauthorization = "{}.{}".format(challenge['token'], thumbprint) acme_dir = acme_dir[:-1] if acme_dir.endswith("/") else acme_dir wellknown_path = "{}/{}".format(acme_dir, challenge['token']) with open(wellknown_path, "w") as wellknown_file: wellknown_file.write(keyauthorization) # check that the file is in place wellknown_url = "http://{}/.well-known/acme-challenge/{}".format( domain, challenge['token']) try: resp = requests.get(wellknown_url) assert resp.strip() == keyauthorization except (requests.HTTPError, AssertionError): os.remove(wellknown_path) raise ValueError( "Wrote file to {}, but couldn't download {}".format( wellknown_path, wellknown_url)) # notify challenge are met code, result = _send_signed_request(challenge['uri'], { "resource": "challenge", "keyAuthorization": keyauthorization, }) if code != 202: raise ValueError("Error triggering challenge: {} {}".format( code, result)) # wait for challenge to be verified while True: try: resp = requests.get(challenge['uri']) challenge_status = resp.json() except requests.HTTPError as e: raise ValueError("Error checking challenge: {} {}".format( e.code, json.loads(e.read()))) if challenge_status['status'] == "pending": time.sleep(2) elif challenge_status['status'] == "valid": sys.stderr.write("verified!\n") os.remove(wellknown_path) break else: raise ValueError("{} challenge did not pass: {}".format( domain, challenge_status)) # get the new certificate info("Signing certificate...") csr_der = openssl("req", "-in", csr, "-outform", "DER", recode=201) code, result = _send_signed_request(CA + "/acme/new-cert", { "resource": "new-cert", "csr": _b64(csr_der), }) # return signed certificate! info("signed!\n") return """-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n""".format( "\n".join(textwrap.wrap( base64.b64encode(result), 64)))