diff options
| -rw-r--r-- | sign_csr.py | 488 | 
1 files changed, 145 insertions, 343 deletions
| 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@<shortest_domain>) -    :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@<shortest_domain>") -    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) | 
