From 6d24382540942818ebde8659ab0d4ea638c354f4 Mon Sep 17 00:00:00 2001 From: rsiddharth Date: Sun, 2 Feb 2020 11:12:55 -0500 Subject: sign_csr.py: Refactor script to use acmev2. Initial version. --- sign_csr.py | 488 ++++++++++++++++++------------------------------------------ 1 file changed, 145 insertions(+), 343 deletions(-) (limited to 'sign_csr.py') diff --git a/sign_csr.py b/sign_csr.py index be9dfd4..17654d3 100644 --- a/sign_csr.py +++ b/sign_csr.py @@ -5,28 +5,22 @@ import argparse, subprocess, json, os, urllib.request, sys, base64, binascii, \ from urllib.request import urlopen from urllib.error import HTTPError -def sign_csr(pubkey, csr, email=None, file_based=False): +def sign_csr(account_key, csr, email=None): """Use the ACME protocol to get an ssl certificate signed by a certificate authority. - :param string pubkey: Path to the user account public key. + :param string account_key: Path to the user account key. :param string csr: Path to the certificate signing request. :param string email: An optional user account contact email (defaults to webmaster@) - :param bool file_based: An optional flag indicating that the - hosting should be file-based rather - than providing a simple python HTTP - server. :returns: Signed Certificate (PEM format) :rtype: string """ - #CA = "https://acme-staging.api.letsencrypt.org" - CA = "https://acme-v01.api.letsencrypt.org" + #CA = "https://acme-staging-v02.api.letsencrypt.org" + CA = "https://acme-v02.api.letsencrypt.org" DIRECTORY = json.loads(urlopen(CA + "/directory").read().decode('utf8')) - nonce_req = urllib.request.Request("{0}/directory".format(CA)) - nonce_req.get_method = lambda : 'HEAD' def _b64(b): "Shortcut function to go from bytes to jwt base64 string" @@ -35,16 +29,63 @@ def sign_csr(pubkey, csr, email=None, file_based=False): return base64.urlsafe_b64encode(b).decode().replace("=", "") + # helper function - run external commands + def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"): + proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate(cmd_input) + if proc.returncode != 0: + raise IOError("{0}\n{1}".format(err_msg, err)) + return out + + # helper function - make request and automatically parse json response + def _do_request(url, data=None, err_msg="Error", depth=0): + try: + resp = urllib.request.urlopen(urllib.request.Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-nosudo"})) + resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers + except IOError as e: + resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e) + code, headers = getattr(e, "code", None), {} + try: + resp_data = json.loads(resp_data) # try to parse json results + except ValueError: + pass # ignore json parsing errors + if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce": + raise IndexError(resp_data) # allow 100 retrys for bad nonces + if code not in [200, 201, 204]: + raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data)) + return resp_data, code, headers + + # helper function - make signed requests + def _send_signed_request(url, payload, err_msg, depth=0): + payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8')) + new_nonce = _do_request(DIRECTORY['newNonce'])[2]['Replay-Nonce'] + protected = {"url": url, "alg": "RS256", "nonce": new_nonce} + protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']}) + protected64 = _b64(json.dumps(protected).encode('utf8')) + protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8') + out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error") + data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)}) + try: + return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth) + except IndexError: # retry bad nonces (they raise IndexError) + return _send_signed_request(url, payload, err_msg, depth=(depth + 1)) + + # helper function - poll until complete + def _poll_until_not(url, pending_statuses, err_msg): + result, t0 = None, time.time() + while result is None or result['status'] in pending_statuses: + assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout + time.sleep(0 if result is None else 2) + result, _, _ = _send_signed_request(url, None, err_msg) + return result + + # Step 1: Get account public key sys.stderr.write("Reading pubkey file...\n") - proc = subprocess.Popen(["openssl", "rsa", "-pubin", "-in", pubkey, "-noout", "-text"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - out, err = proc.communicate() - if proc.returncode != 0: - raise IOError("Error loading {0}".format(pubkey)) + out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="Error reading account public key") pub_hex, pub_exp = re.search( - "Modulus(?: \((?:2048|4096) bit\)|)\:\s+00:([a-f0-9\:\s]+?)Exponent\: ([0-9]+)", - out, re.MULTILINE|re.DOTALL).groups() + r"modulus:[\s]+?00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() pub_mod = binascii.unhexlify(re.sub("(\s|:)", "", pub_hex)) pub_mod64 = _b64(pub_mod) pub_exp = int(pub_exp) @@ -52,30 +93,23 @@ def sign_csr(pubkey, csr, email=None, file_based=False): pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp pub_exp = binascii.unhexlify(pub_exp) pub_exp64 = _b64(pub_exp) - header = { - "alg": "RS256", - "jwk": { - "e": pub_exp64, - "kty": "RSA", - "n": pub_mod64, - }, + jwk = { + "e": pub_exp64, + "kty": "RSA", + "n": pub_mod64, } - accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':')) + accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':')) thumbprint = _b64(hashlib.sha256(accountkey_json.encode()).digest()) sys.stderr.write("Found public key!\n") # Step 2: Get the domain names to be certified sys.stderr.write("Reading csr file...\n") - proc = subprocess.Popen(["openssl", "req", "-in", csr, "-noout", "-text"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - out, err = proc.communicate() - if proc.returncode != 0: - raise IOError("Error loading {0}".format(csr)) + out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {}".format(csr)) domains = set([]) - common_name = re.search("Subject:.*? CN *= *([^\s,;/]+)", out) + common_name = re.search("Subject:.*? CN *= *([^\s,;/]+)", out.decode('utf8')) 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) + subject_alt_names = re.search("X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) if subject_alt_names is not None: for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): @@ -91,328 +125,97 @@ def sign_csr(pubkey, csr, email=None, file_based=False): email = input_email if input_email else default_email sys.stdout = stdout - # Step 4: Generate the payloads that need to be signed - # registration - sys.stderr.write("Building request payloads...\n") - reg_nonce = urllib.request.urlopen(nonce_req).headers['Replay-Nonce'] - reg_raw = json.dumps({ - "resource": "new-reg", - "contact": ["mailto:{0}".format(email)], - "agreement": DIRECTORY['meta']['terms-of-service'], - }, sort_keys=True, indent=4) - reg_b64 = _b64(reg_raw) - reg_protected = copy.deepcopy(header) - reg_protected.update({"nonce": reg_nonce}) - reg_protected64 = _b64(json.dumps(reg_protected, sort_keys=True, indent=4)) - reg_file = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".json") - reg_file.write("{0}.{1}".format(reg_protected64, reg_b64).encode()) - reg_file.flush() - reg_file_name = os.path.basename(reg_file.name) - reg_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".sig") - reg_file_sig_name = os.path.basename(reg_file_sig.name) - - # need signature for each domain identifiers - ids = [] + + # Step 4: Generate the payload for registering user and initiate registration. + sys.stderr.write("Registering {0}...\n".format(email)) + reg = { + "termsOfServiceAgreed": True + } + acct_headers = None + result, code, acct_headers = _send_signed_request(DIRECTORY['newAccount'], reg, "Error registering") + if code == 201: + sys.stderr.write("Registered!\n") + else: + sys.stderr.write("Already registered!\n") + + + # Step 5: Request challenges for each domain for domain in domains: - sys.stderr.write("Building request for {0}...\n".format(domain)) - id_nonce = urllib.request.urlopen(nonce_req).headers['Replay-Nonce'] - id_raw = json.dumps({ - "resource": "new-authz", - "identifier": { + sys.stderr.write("Making new order for {0}...\n".format(domain)) + id = { + "identifiers": [{ "type": "dns", "value": domain, - }, - }, sort_keys=True) - id_b64 = _b64(id_raw) - id_protected = copy.deepcopy(header) - id_protected.update({"nonce": id_nonce}) - id_protected64 = _b64(json.dumps(id_protected, sort_keys=True, indent=4)) - id_file = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".json") - id_file.write("{0}.{1}".format(id_protected64, id_b64).encode()) - id_file.flush() - id_file_name = os.path.basename(id_file.name) - id_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".sig") - id_file_sig_name = os.path.basename(id_file_sig.name) - ids.append({ - "domain": domain, - "protected64": id_protected64, - "data64": id_b64, - "file": id_file, - "file_name": id_file_name, - "sig": id_file_sig, - "sig_name": id_file_sig_name, - }) - - # need signature for the final certificate issuance - sys.stderr.write("Building request for CSR...\n") - proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - csr_der, err = proc.communicate() - csr_der64 = _b64(csr_der) - csr_nonce = urllib.request.urlopen(nonce_req).headers['Replay-Nonce'] - csr_raw = json.dumps({ - "resource": "new-cert", - "csr": csr_der64, - }, sort_keys=True, indent=4) - csr_b64 = _b64(csr_raw) - csr_protected = copy.deepcopy(header) - csr_protected.update({"nonce": csr_nonce}) - csr_protected64 = _b64(json.dumps(csr_protected, sort_keys=True, indent=4)) - csr_file = tempfile.NamedTemporaryFile(dir=".", prefix="cert_", suffix=".json") - csr_file.write("{0}.{1}".format(csr_protected64, csr_b64).encode()) - csr_file.flush() - csr_file_name = os.path.basename(csr_file.name) - csr_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="cert_", suffix=".sig") - csr_file_sig_name = os.path.basename(csr_file_sig.name) - - # Step 5: Ask the user to sign the registration and requests - sys.stderr.write("""\ -STEP 2: You need to sign some files (replace 'user.key' with your user private key). - -openssl dgst -sha256 -sign user.key -out {0} {1} -{2} -openssl dgst -sha256 -sign user.key -out {3} {4} - -""".format( - reg_file_sig_name, reg_file_name, - "\n".join("openssl dgst -sha256 -sign user.key -out {0} {1}".format(i['sig_name'], i['file_name']) for i in ids), - csr_file_sig_name, csr_file_name)) - - stdout = sys.stdout - sys.stdout = sys.stderr - input("Press Enter when you've run the above commands in a new terminal window...") - sys.stdout = stdout - - # Step 6: Load the signatures - reg_file_sig.seek(0) - reg_sig64 = _b64(reg_file_sig.read()) - for n, i in enumerate(ids): - i['sig'].seek(0) - i['sig64'] = _b64(i['sig'].read()) - - # Step 7: Register the user - sys.stderr.write("Registering {0}...\n".format(email)) - reg_data = json.dumps({ - "header": header, - "protected": reg_protected64, - "payload": reg_b64, - "signature": reg_sig64, - }, sort_keys=True, indent=4) - reg_url = "{0}/acme/new-reg".format(CA) - try: - resp = urllib.request.urlopen(reg_url, reg_data.encode()) - result = json.loads(resp.read()) - except HTTPError as e: - err = e.read() - # skip already registered accounts - if b"Registration key is already in use" in err: - sys.stderr.write("Already registered. Skipping...\n") - else: - sys.stderr.write("Error: reg_data:\n") - sys.stderr.write("POST {0}\n".format(reg_url)) - sys.stderr.write(reg_data) - sys.stderr.write("\n") - sys.stderr.write(err.decode()) - sys.stderr.write("\n") - raise - - # Step 8: Request challenges for each domain - responses = [] - tests = [] - for n, i in enumerate(ids): - sys.stderr.write("Requesting challenges for {0}...\n".format(i['domain'])) - id_data = json.dumps({ - "header": header, - "protected": i['protected64'], - "payload": i['data64'], - "signature": i['sig64'], - }, sort_keys=True, indent=4) - id_url = "{0}/acme/new-authz".format(CA) - try: - resp = urllib.request.urlopen(id_url, id_data.encode()) - result = json.loads(resp.read()) - except HTTPError as e: - sys.stderr.write("Error: id_data:\n") - sys.stderr.write("POST {0}\n".format(id_url)) - sys.stderr.write(id_data) - sys.stderr.write("\n") - sys.stderr.write(e.read()) - sys.stderr.write("\n") - raise - challenge = [c for c in result['challenges'] if c['type'] == "http-01"][0] + }], + } + order, order_code, order_headers = _send_signed_request(DIRECTORY['newOrder'], id, "Error creating new order") + + # Request challenges + sys.stderr.write("Requesting challenges...\n") + chl_result, chl_code, chl_headers = _send_signed_request(order['authorizations'][0], None, "Error getting challenges") + + challenge = [c for c in chl_result['challenges'] if c['type'] == "http-01"][0] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) keyauthorization = "{0}.{1}".format(challenge['token'], thumbprint) - # challenge request - sys.stderr.write("Building challenge responses for {0}...\n".format(i['domain'])) - test_nonce = urllib.request.urlopen(nonce_req).headers['Replay-Nonce'] - test_raw = json.dumps({ - "resource": "challenge", - "keyAuthorization": keyauthorization, - }, sort_keys=True, indent=4) - test_b64 = _b64(test_raw) - test_protected = copy.deepcopy(header) - test_protected.update({"nonce": test_nonce}) - test_protected64 = _b64(json.dumps(test_protected, sort_keys=True, indent=4)) - test_file = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".json") - test_file.write("{0}.{1}".format(test_protected64, test_b64).encode()) - test_file.flush() - test_file_name = os.path.basename(test_file.name) - test_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".sig") - test_file_sig_name = os.path.basename(test_file_sig.name) - tests.append({ - "uri": challenge['uri'], - "protected64": test_protected64, - "data64": test_b64, - "file": test_file, - "file_name": test_file_name, - "sig": test_file_sig, - "sig_name": test_file_sig_name, - }) + # build request for the server to test this challenge. + test_url = challenge['url'] + test_raw = "{}" # challenge response for server - responses.append({ + response = { "uri": ".well-known/acme-challenge/{0}".format(challenge['token']), "data": keyauthorization, - }) - - # Step 9: Ask the user to sign the challenge responses - sys.stderr.write("""\ -STEP 3: You need to sign some more files (replace 'user.key' with your user private key). - -{0} - -""".format( - "\n".join("openssl dgst -sha256 -sign user.key -out {0} {1}".format( - i['sig_name'], i['file_name']) for i in tests))) - - stdout = sys.stdout - sys.stdout = sys.stderr - input("Press Enter when you've run the above commands in a new terminal window...") - sys.stdout = stdout + } - # Step 10: Load the response signatures - for n, i in enumerate(ids): - tests[n]['sig'].seek(0) - tests[n]['sig64'] = _b64(tests[n]['sig'].read()) - - # Step 11: Ask the user to host the token on their server - for n, i in enumerate(ids): - if file_based: - sys.stderr.write("""\ -STEP {0}: Please update your server to serve the following file at this URL: + # Step 6: Ask the user to host the token on their server + sys.stderr.write("""\ +Please update your server to serve the following file at this URL: -------------- -URL: http://{1}/{2} -File contents: \"{3}\" +URL: http://{0}/{1} +File contents: \"{2}\" -------------- Notes: - Do not include the quotes in the file. - The file should be one line without any spaces. -""".format(n + 4, i['domain'], responses[n]['uri'], responses[n]['data'])) +""".format(domain, response['uri'], response['data'])) - stdout = sys.stdout - sys.stdout = sys.stderr - input("Press Enter when you've got the file hosted on your server...") - sys.stdout = stdout + stdout = sys.stdout + sys.stdout = sys.stderr + input("Press Enter when you've got the file hosted on your server...") + sys.stdout = stdout + + # Step 7: Let the CA know you're ready for the challenge + sys.stderr.write("Requesting verification for {0}...\n".format(domain)) + _send_signed_request(challenge['url'], {}, "Error requesting challenge verfication: {0}".format(domain)) + chl_verification = _poll_until_not(challenge['url'], ["pending"], "Error checking challenge verification") + if chl_verification['status'] != "valid": + raise ValueError("Challenge did not pass for {0}: {1}".format(domain, chl_verification)) + sys.stderr.write("{} verified!\n".format(domain)) + + # Step 8: Finalize + csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error") + fnlz_resp, fnlz_code, fnlz_headers = _send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order") + + # Step 9: Wait for CA to mark test as valid + sys.stderr.write("Waiting for {0} challenge to pass...\n".format(domain)) + order = _poll_until_not(order_headers['Location'], ["pending", "processing"], "Error checking order status") + + if order['status'] == "valid": + sys.stderr.write("Passed {0} challenge!\n".format(domain)) else: - sys.stderr.write("""\ -STEP {0}: You need to run this command on {1} (don't stop the python command until the next step). - -sudo python -c "import BaseHTTPServer; \\ - h = BaseHTTPServer.BaseHTTPRequestHandler; \\ - h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('{2}'); \\ - s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \\ - s.serve_forever()" - -""".format(n + 4, i['domain'], responses[n]['data'])) - - stdout = sys.stdout - sys.stdout = sys.stderr - input("Press Enter when you've got the python command running on your server...") - sys.stdout = stdout - - # Step 12: Let the CA know you're ready for the challenge - sys.stderr.write("Requesting verification for {0}...\n".format(i['domain'])) - test_data = json.dumps({ - "header": header, - "protected": tests[n]['protected64'], - "payload": tests[n]['data64'], - "signature": tests[n]['sig64'], - }, sort_keys=True, indent=4) - test_url = tests[n]['uri'] - try: - resp = urllib.request.urlopen(test_url, test_data.encode()) - test_result = json.loads(resp.read()) - except HTTPError as e: - sys.stderr.write("Error: test_data:\n") - sys.stderr.write("POST {0}\n".format(test_url)) - sys.stderr.write(test_data) - sys.stderr.write("\n") - sys.stderr.write(e.read().decode()) - sys.stderr.write("\n") - raise - - # Step 13: Wait for CA to mark test as valid - sys.stderr.write("Waiting for {0} challenge to pass...\n".format(i['domain'])) - while True: - try: - resp = urllib.request.urlopen(test_url) - challenge_status = json.loads(resp.read()) - except HTTPError as e: - sys.stderr.write("Error: test_data:\n") - sys.stderr.write("GET {0}\n".format(test_url)) - sys.stderr.write(test_data) - sys.stderr.write("\n") - sys.stderr.write(e.read()) - sys.stderr.write("\n") - raise - if challenge_status['status'] == "pending": - time.sleep(2) - elif challenge_status['status'] == "valid": - sys.stderr.write("Passed {0} challenge!\n".format(i['domain'])) - break - else: - raise KeyError("'{0}' challenge did not pass: {1}".format(i['domain'], challenge_status)) - - # Step 14: Get the certificate signed - sys.stderr.write("Requesting signature...\n") - csr_file_sig.seek(0) - csr_sig64 = _b64(csr_file_sig.read()) - csr_data = json.dumps({ - "header": header, - "protected": csr_protected64, - "payload": csr_b64, - "signature": csr_sig64, - }, sort_keys=True, indent=4) - csr_url = "{0}/acme/new-cert".format(CA) - try: - resp = urllib.request.urlopen(csr_url, csr_data.encode()) - signed_der = resp.read() - except HTTPError as e: - sys.stderr.write("Error: csr_data:\n") - sys.stderr.write("POST {0}\n".format(csr_url)) - sys.stderr.write(csr_data) - sys.stderr.write("\n") - sys.stderr.write(e.read()) - sys.stderr.write("\n") - raise - - # Step 15: Convert the signed cert from DER to PEM - sys.stderr.write("Certificate signed!\n") - - if file_based: - sys.stderr.write("You can remove the acme-challenge file from your webserver now.\n") - else: - sys.stderr.write("You can stop running the python command on your server (Ctrl+C works).\n") + raise ValueError("'{0}' challenge did not pass: {1}".format(domain, order)) + + + # Step 10: Get the certificate. + sys.stderr.write("Getting certificate...\n") + signed_pem, _, _ = _send_signed_request(order['certificate'], None, "Error getting certificate") - signed_der64 = base64.b64encode(signed_der) - signed_pem = """\ ------BEGIN CERTIFICATE----- -{0} ------END CERTIFICATE----- -""".format("\n".join(textwrap.wrap(signed_der64.decode(), 64))) + sys.stderr.write("Received certificate!\n") + sys.stderr.write("You can remove the acme-challenge file from your webserver now.\n") return signed_pem @@ -420,17 +223,17 @@ if __name__ == "__main__": parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description="""\ -Get a SSL certificate signed by a Let's Encrypt (ACME) certificate authority and -output that signed certificate. You do NOT need to run this script on your -server and this script does not ask for your private keys. It will print out -commands that you need to run with your private key or on your server as root, -which gives you a chance to review the commands instead of trusting this script. +Get a SSL certificate signed by a Let's Encrypt (ACME) certificate +authority and output that signed certificate. You do NOT need to run +this script on your server, it is meant to be run on your +computer. The script will request you to manually deploy the acme +challenge on your server. NOTE: YOUR ACCOUNT KEY NEEDS TO BE DIFFERENT FROM YOUR DOMAIN KEY. Prerequisites: * openssl -* python +* python version 3 Example: Generate an account keypair, a domain key and csr, and have the domain csr signed. -------------- @@ -438,16 +241,15 @@ $ openssl genrsa 4096 > user.key $ openssl rsa -in user.key -pubout > user.pub $ openssl genrsa 4096 > domain.key $ openssl req -new -sha256 -key domain.key -subj "/CN=example.com" > domain.csr -$ python sign_csr.py --public-key user.pub domain.csr > signed.crt +$ python3 sign_csr.py --account-key user.key --email user@example.com domain.csr > signed.crt -------------- """) - parser.add_argument("-p", "--public-key", required=True, help="path to your account public key") + parser.add_argument("-k", "--account-key", required=True, help="path to your Let's Encrypt account private key") parser.add_argument("-e", "--email", default=None, help="contact email, default is webmaster@") - parser.add_argument("-f", "--file-based", action='store_true', help="if set, a file-based response is used") parser.add_argument("csr_path", help="path to your certificate signing request") args = parser.parse_args() - signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based) + signed_crt = sign_csr(args.account_key, args.csr_path, email=args.email) sys.stdout.write(signed_crt) -- cgit v1.2.3