|
13 | 13 | LOGGER.addHandler(logging.StreamHandler()) |
14 | 14 | LOGGER.setLevel(logging.INFO) |
15 | 15 |
|
16 | | -def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None, check_port=None): |
| 16 | +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, |
| 17 | + directory_url=DEFAULT_DIRECTORY_URL, contact=None, check_port=None, challenge_type="http", challenge_script=None): |
17 | 18 | directory, acct_headers, alg, jwk = None, None, None, None # global variables |
18 | 19 |
|
19 | 20 | # helper functions - base64 encode for jose spec |
@@ -70,6 +71,45 @@ def _poll_until_not(url, pending_statuses, err_msg): |
70 | 71 | result, _, _ = _send_signed_request(url, None, err_msg) |
71 | 72 | return result |
72 | 73 |
|
| 74 | + def _challenge_http(authorization, thumbprint, acme_dir, disable_check): |
| 75 | + # find the http-01 challenge and write the challenge file |
| 76 | + challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0] |
| 77 | + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) |
| 78 | + keyauthorization = "{0}.{1}".format(token, thumbprint) |
| 79 | + wellknown_path = os.path.join(acme_dir, token) |
| 80 | + with open(wellknown_path, "w") as wellknown_file: |
| 81 | + wellknown_file.write(keyauthorization) |
| 82 | + |
| 83 | + # check that the file is in place |
| 84 | + domain = authorization['identifier']['value'] |
| 85 | + try: |
| 86 | + wellknown_url = "http://{0}{1}/.well-known/acme-challenge/{2}".format(domain, "" if check_port is None else ":{0}".format(check_port), token) |
| 87 | + assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization) |
| 88 | + except (AssertionError, ValueError) as e: |
| 89 | + raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e)) |
| 90 | + return challenge, wellknown_path |
| 91 | + |
| 92 | + def _challenge_dns(authorization, thumbprint, challenge_script): |
| 93 | + challenge = [c for c in authorization['challenges'] if c['type'] == "dns-01"][0] |
| 94 | + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) |
| 95 | + keyauthorization = "{0}.{1}".format(token, thumbprint) |
| 96 | + txtrecord = _b64(hashlib.sha256(keyauthorization.encode('utf8')).digest()) |
| 97 | + domain = authorization['identifier']['value'] |
| 98 | + subprocess.call([challenge_script, "--add", "--domain", domain, txtrecord]) |
| 99 | + try: |
| 100 | + subprocess.call(["host", "-t", "TXT", "_acme-challenge.{0}".format(domain)]) |
| 101 | + assert(disable_check or True) # TODO |
| 102 | + except AssertionError: |
| 103 | + subprocess.call([challenge_script, "--remove", "--domain", domain, txtrecord]) |
| 104 | + raise ValueError("Set up the DNS challenge, but couldn't verify: {0}".format(e)) |
| 105 | + return challenge, txtrecord |
| 106 | + |
| 107 | + if challenge_type not in ("http", "dns"): |
| 108 | + raise ValueError("Unsupported challenge type: {0}".format(challenge_type)) |
| 109 | + |
| 110 | + if challenge_type == "dns" and challenge_script is None: |
| 111 | + raise ValueError("Challenge script is required for dns challenge") |
| 112 | + |
73 | 113 | # parse account key to get public key |
74 | 114 | log.info("Parsing account key...") |
75 | 115 | out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error") |
@@ -131,27 +171,20 @@ def _poll_until_not(url, pending_statuses, err_msg): |
131 | 171 | continue |
132 | 172 | log.info("Verifying {0}...".format(domain)) |
133 | 173 |
|
134 | | - # find the http-01 challenge and write the challenge file |
135 | | - challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0] |
136 | | - token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) |
137 | | - keyauthorization = "{0}.{1}".format(token, thumbprint) |
138 | | - wellknown_path = os.path.join(acme_dir, token) |
139 | | - with open(wellknown_path, "w") as wellknown_file: |
140 | | - wellknown_file.write(keyauthorization) |
141 | | - |
142 | | - # check that the file is in place |
143 | | - try: |
144 | | - wellknown_url = "http://{0}{1}/.well-known/acme-challenge/{2}".format(domain, "" if check_port is None else ":{0}".format(check_port), token) |
145 | | - assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization) |
146 | | - except (AssertionError, ValueError) as e: |
147 | | - raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e)) |
| 174 | + if challenge_type == "http": |
| 175 | + challenge, wellknown_path = _challenge_http(authorization, thumbprint, acme_dir, disable_check) |
| 176 | + elif challenge_type == "dns": |
| 177 | + challenge, txtrecord = _challenge_dns(authorization, thumbprint, challenge_script) |
148 | 178 |
|
149 | 179 | # say the challenge is done |
150 | 180 | _send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain)) |
151 | 181 | authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain)) |
152 | 182 | if authorization['status'] != "valid": |
153 | 183 | raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization)) |
154 | | - os.remove(wellknown_path) |
| 184 | + if challenge_type == "http": |
| 185 | + os.remove(wellknown_path) |
| 186 | + elif challenge_type == "dns": |
| 187 | + subprocess.call([challenge_script, "--remove", "--domain", domain, txtrecord]) |
155 | 188 | log.info("{0} verified!".format(domain)) |
156 | 189 |
|
157 | 190 | # finalize the order with the csr |
@@ -189,10 +222,13 @@ def main(argv=None): |
189 | 222 | parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!") |
190 | 223 | parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key") |
191 | 224 | parser.add_argument("--check-port", metavar="PORT", default=None, help="what port to use when self-checking the challenge file, default is port 80") |
| 225 | + parser.add_argument("--challenge-type", required=False, default="http", help="type of ACME challenge, supported: http, dns") |
| 226 | + parser.add_argument("--challenge-script", required=False, default=None, help="script to set up challenge on the server (required for dns challenge)") |
192 | 227 |
|
193 | 228 | args = parser.parse_args(argv) |
194 | 229 | LOGGER.setLevel(args.quiet or LOGGER.level) |
195 | | - signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact, check_port=args.check_port) |
| 230 | + signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, |
| 231 | + directory_url=args.directory_url, contact=args.contact, check_port=args.check_port, challenge_type=args.challenge_type, challenge_script=args.challenge_script) |
196 | 232 | sys.stdout.write(signed_crt) |
197 | 233 |
|
198 | 234 | if __name__ == "__main__": # pragma: no cover |
|
0 commit comments