Skip to content

Commit 7f1e0e6

Browse files
committed
Add support for dns challenge
Since there is no single interface for administrating DNS servers a custom script (specified with --challenge-script) is called by acme_tiny.py. The script needs to support the following interface: challenge_script (--add|--remove) --domain DOMAIN TXTRECORD --add - add a TXT record --remove - remove a TXT record --domain DOMAIN - specify a domain name TXTRECORD - value of a TXT record to be added or removed Patch taken from: diafygi#238 Signed-off-by: Ulrich Weber <ulrich.weber@gmail.com>
1 parent 1b61d30 commit 7f1e0e6

1 file changed

Lines changed: 53 additions & 17 deletions

File tree

acme_tiny.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
LOGGER.addHandler(logging.StreamHandler())
1414
LOGGER.setLevel(logging.INFO)
1515

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):
1718
directory, acct_headers, alg, jwk = None, None, None, None # global variables
1819

1920
# helper functions - base64 encode for jose spec
@@ -70,6 +71,45 @@ def _poll_until_not(url, pending_statuses, err_msg):
7071
result, _, _ = _send_signed_request(url, None, err_msg)
7172
return result
7273

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+
73113
# parse account key to get public key
74114
log.info("Parsing account key...")
75115
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):
131171
continue
132172
log.info("Verifying {0}...".format(domain))
133173

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)
148178

149179
# say the challenge is done
150180
_send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain))
151181
authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain))
152182
if authorization['status'] != "valid":
153183
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])
155188
log.info("{0} verified!".format(domain))
156189

157190
# finalize the order with the csr
@@ -189,10 +222,13 @@ def main(argv=None):
189222
parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!")
190223
parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key")
191224
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)")
192227

193228
args = parser.parse_args(argv)
194229
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)
196232
sys.stdout.write(signed_crt)
197233

198234
if __name__ == "__main__": # pragma: no cover

0 commit comments

Comments
 (0)