From 3b1ab81b0f58ebaf66ed3f0fa1a49ff53ed20b40 Mon Sep 17 00:00:00 2001 From: Daniel Roesler Date: Sun, 14 Jun 2015 10:32:01 -0700 Subject: fixed #3, added support for multiple domains via subject alt names. also moved to SimpleHTTP with tls off --- README.md | 58 +++++++-------- sign_csr.py | 235 ++++++++++++++++++++++++++++++++++-------------------------- 2 files changed, 162 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 3a0ad8e..64f6a79 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,13 @@ This is the key that you will get signed for free for your domain (replace and CSR for your domain, you can skip this step. ```sh +#Create a CSR for example.com openssl genrsa 4096 > domain.key openssl req -new -sha256 -key domain.key -subj "/CN=example.com" > domain.csr + +#Alternatively, if you want both example.com and www.example.com +openssl genrsa 4096 > domain.key +openssl req -new -sha256 -key domain.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:example.com,DNS:www.example.com")) > domain.csr ``` Third, you run the script using python and passing in the path to your user @@ -150,34 +155,32 @@ user@hostname:~$ python sign_csr.py user.pub domain.csr > signed.crt Reading pubkey file... Found public key! Reading csr file... -Found domain 'letsencrypt.daylightpirates.org' +Found domains letsencrypt.daylightpirates.org -STEP 1: You need to sign some files (replace 'user.key' with your account private key). +STEP 1: You need to sign some files (replace 'user.key' with your user private key). -openssl dgst -sha256 -sign user.key -out register_TeH8Cg.sig register_jzlzOk.json -openssl dgst -sha256 -sign user.key -out domain_FOtDWV.sig domain_dkaWo7.json -openssl dgst -sha256 -sign user.key -out challenge_IxfeES.sig challenge_svyiIw.json +openssl dgst -sha256 -sign user.key -out register_TYtLJT.sig register_i3UGRo.json +openssl dgst -sha256 -sign user.key -out domain_ZdDFx2.sig domain_F5CAvm.json +openssl dgst -sha256 -sign user.key -out challenge_NF5S_I.sig challenge_ETkPkW.json Press Enter when you've run the above commands in a new terminal window... -Registering... -Requesting challenges... +Registering webmaster@letsencrypt.daylightpirates.org... +Requesting challenges for letsencrypt.daylightpirates.org... -STEP 2: You need to run these two commands on letsencrypt.daylightpirates.org (don't stop the python command). +STEP 2: You need to run these two commands on letsencrypt.daylightpirates.org (don't stop the python command until the next step). -openssl req -new -newkey rsa:2048 -days 365 -subj "/CN=a" -nodes -x509 -keyout a.key -out a.crt -sudo python -c "import BaseHTTPServer, ssl; \ +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('Zz7fkg1LAAxOomahwF5jP67ZJDFjQSkUDPAbLIMCtvY'); \ - s = BaseHTTPServer.HTTPServer(('0.0.0.0', 443), h); \ - s.socket = ssl.wrap_socket(s.socket, keyfile='a.key', certfile='a.crt'); \ + h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('b636mznlTFh4wNaY2R6Px1nsKykhyGzC7siaO_Mf7zA'); \ + s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \ s.serve_forever()" Press Enter when you've got the python command running on your server... -Requesting verification... +Requesting verification for letsencrypt.daylightpirates.org... -STEP 3: You need to sign one more file (replace 'user.key' with your account private key). +FINAL STEP: You need to sign one more file (replace 'user.key' with your user private key). -openssl dgst -sha256 -sign user.key -out cert_97g7hU.sig cert_dDbKbs.json +openssl dgst -sha256 -sign user.key -out cert_NWCQzv.sig cert_QQJGmK.json Press Enter when you've run the above command in a new terminal window... Requesting signature... @@ -220,29 +223,22 @@ user@hostname:~$ ###Manual Commands (the stuff the script asked you to do in a 2nd terminal) ``` #first set of signed files -user@hostname:~$ openssl dgst -sha256 -sign user.key -out register_TeH8Cg.sig register_jzlzOk.json -user@hostname:~$ openssl dgst -sha256 -sign user.key -out domain_FOtDWV.sig domain_dkaWo7.json -user@hostname:~$ openssl dgst -sha256 -sign user.key -out challenge_IxfeES.sig challenge_svyiIw.json +user@hostname:~$ openssl dgst -sha256 -sign user.key -out register_TYtLJT.sig register_i3UGRo.json +user@hostname:~$ openssl dgst -sha256 -sign user.key -out domain_ZdDFx2.sig domain_F5CAvm.json +user@hostname:~$ openssl dgst -sha256 -sign user.key -out challenge_NF5S_I.sig challenge_ETkPkW.json user@hostname:~$ #second set of signed files -user@hostname:~$ openssl dgst -sha256 -sign user.key -out cert_97g7hU.sig cert_dDbKbs.json +user@hostname:~$ openssl dgst -sha256 -sign user.key -out cert_NWCQzv.sig cert_QQJGmK.json user@hostname:~$ ``` ###Server Commands (the stuff the script asked you to do on your server) ``` -ubuntu@letsencrypt.daylightpirates.org:~$ openssl req -new -newkey rsa:2048 -days 365 -subj "/CN=a" -nodes -x509 -keyout a.key -out a.crt -Generating a 2048 bit RSA private key -........................+++ -.....+++ -writing new private key to 'a.key' ------ -ubuntu@letsencrypt.daylightpirates.org:~$ sudo python -c "import BaseHTTPServer, ssl; \ +ubuntu@letsencrypt.daylightpirates.org:~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('Zz7fkg1LAAxOomahwF5jP67ZJDFjQSkUDPAbLIMCtvY'); \ -> s = BaseHTTPServer.HTTPServer(('0.0.0.0', 443), h); \ -> s.socket = ssl.wrap_socket(s.socket, keyfile='a.key', certfile='a.crt'); \ +> h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('b636mznlTFh4wNaY2R6Px1nsKykhyGzC7siaO_Mf7zA'); \ +> s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \ > s.serve_forever()" 54.183.196.250 - - [11/Jun/2015 16:07:45] "GET /.well-known/acme-challenge/Abc46LNljZ5zjen6f-mcCA HTTP/1.1" 200 - ^CTraceback (most recent call last): @@ -296,7 +292,7 @@ better. The script itself, `sign_csr.py`, is less than 300 lines of code, so feel free to read through it! I tried to comment things well and make it crystal clear what it's doing. -For example, it currently can't do any ACME challenges besides SimpleHTTPS. Maybe +For example, it currently can't do any ACME challenges besides SimpleHTTP. Maybe someone could do a pull request to add more challenge compatibility? Also, it currently can't revoke certificates, and I don't want to include that in the `sign_csr.py` script. Perhaps there should also be a `revoke_crt.py` script? diff --git a/sign_csr.py b/sign_csr.py index d24714a..bd840cb 100644 --- a/sign_csr.py +++ b/sign_csr.py @@ -49,22 +49,31 @@ def sign_csr(pubkey, csr): } sys.stderr.write("Found public key!\n".format(header)) - #Step 2: Get the domain name to be certified + #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) out, err = proc.communicate() if proc.returncode != 0: raise IOError("Error loading {}".format(csr)) - domain = re.search("Subject:.*? CN=([^\s,;/]+).*?", out, re.MULTILINE|re.DOTALL).groups()[0] - sys.stderr.write("Found domain '{}'\n".format(domain)) + domains = set([]) + common_name = re.search("Subject:.*? CN=([^\s,;/]+)", out) + if common_name is not None: + domains.add(common_name) + 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:]) + sys.stderr.write("Found domains {}\n".format(", ".join(domains))) #Step 2: Generate the payloads that need to be signed #registration - reg_email = "webmaster@{}".format(domain) + reg_email = "webmaster@{}".format(min(domains, key=len)) reg_raw = json.dumps({ "contact": ["mailto:{}".format(reg_email)], "agreement": "https://www.letsencrypt-demo.org/terms", + #"agreement": "https://letsencrypt.org/be-good", }, sort_keys=True, indent=4) reg_b64 = _b64(reg_raw) try: @@ -81,56 +90,79 @@ def sign_csr(pubkey, csr): reg_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".sig") reg_file_sig_name = os.path.basename(reg_file_sig.name) - #identifier - id_raw = json.dumps({"identifier": {"type": "dns", "value": domain}}, sort_keys=True) - id_b64 = _b64(id_raw) - try: - urllib2.urlopen(nonce_req).info() - except urllib2.HTTPError as e: - id_nonce = json.dumps({ - "nonce": e.hdrs.get("replay-nonce", _b64(os.urandom(16))), + #need signature for each domain identifier and challenge + ids = [] + tests = [] + for domain in domains: + + #identifier + id_raw = json.dumps({"identifier": {"type": "dns", "value": domain}}, sort_keys=True) + id_b64 = _b64(id_raw) + try: + urllib2.urlopen(nonce_req).info() + except urllib2.HTTPError as e: + id_nonce = json.dumps({ + "nonce": e.hdrs.get("replay-nonce", _b64(os.urandom(16))), + }, sort_keys=True, indent=4) + id_nonce64 = _b64(id_nonce) + id_file = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".json") + id_file.write("{}.{}".format(id_nonce64, id_b64)) + 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, + "nonce64": id_nonce64, + "data64": id_b64, + "file": id_file, + "file_name": id_file_name, + "sig": id_file_sig, + "sig_name": id_file_sig_name, + }) + + #challenge + test_path = _b64(os.urandom(16)) + test_raw = json.dumps({ + "type": "simpleHttp", + "path": test_path, + "tls": False, }, sort_keys=True, indent=4) - id_nonce64 = _b64(id_nonce) - id_file = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".json") - id_file.write("{}.{}".format(id_nonce64, id_b64)) - 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) - - #challenge - test_path = _b64(os.urandom(16)) - test_raw = json.dumps({ - "type": "simpleHttps", - "path": test_path, - }, sort_keys=True, indent=4) - test_b64 = _b64(test_raw) - try: - urllib2.urlopen(nonce_req).info() - except urllib2.HTTPError as e: - test_nonce = json.dumps({ - "nonce": e.hdrs.get("replay-nonce", _b64(os.urandom(16))), - }, sort_keys=True, indent=4) - test_nonce64 = _b64(test_nonce) - test_file = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".json") - test_file.write("{}.{}".format(test_nonce64, test_b64)) - 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) + test_b64 = _b64(test_raw) + try: + urllib2.urlopen(nonce_req).info() + except urllib2.HTTPError as e: + test_nonce = json.dumps({ + "nonce": e.hdrs.get("replay-nonce", _b64(os.urandom(16))), + }, sort_keys=True, indent=4) + test_nonce64 = _b64(test_nonce) + test_file = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".json") + test_file.write("{}.{}".format(test_nonce64, test_b64)) + 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({ + "nonce64": test_nonce64, + "data64": test_b64, + "file": test_file, + "file_name": test_file_name, + "sig": test_file_sig, + "sig_name": test_file_sig_name, + }) #Step 3: Ask the user to sign the payloads sys.stderr.write(""" STEP 1: You need to sign some files (replace 'user.key' with your user private key). openssl dgst -sha256 -sign user.key -out {} {} -openssl dgst -sha256 -sign user.key -out {} {} -openssl dgst -sha256 -sign user.key -out {} {} +{} +{} """.format( reg_file_sig_name, reg_file_name, - id_file_sig_name, id_file_name, - test_file_sig_name, test_file_name)) + "\n".join("openssl dgst -sha256 -sign user.key -out {} {}".format(i['sig_name'], i['file_name']) for i in ids), + "\n".join("openssl dgst -sha256 -sign user.key -out {} {}".format(i['sig_name'], i['file_name']) for i in tests))) stdout = sys.stdout sys.stdout = sys.stderr @@ -140,10 +172,11 @@ openssl dgst -sha256 -sign user.key -out {} {} #Step 4: Load the signatures reg_file_sig.seek(0) reg_sig64 = _b64(reg_file_sig.read()) - id_file_sig.seek(0) - id_sig64 = _b64(id_file_sig.read()) - test_file_sig.seek(0) - test_sig64 = _b64(test_file_sig.read()) + for n, i in enumerate(ids): + i['sig'].seek(0) + i['sig64'] = _b64(i['sig'].read()) + tests[n]['sig'].seek(0) + tests[n]['sig64'] = _b64(tests[n]['sig'].read()) #Step 5: Register the user sys.stderr.write("Registering {}...\n".format(reg_email)) @@ -169,73 +202,75 @@ openssl dgst -sha256 -sign user.key -out {} {} sys.stderr.write("\n") raise - #Step 6: Get simpleHttps challenge token - sys.stderr.write("Requesting challenges...\n") - id_data = json.dumps({ - "header": header, - "protected": id_nonce64, - "payload": id_b64, - "signature": id_sig64, - }, sort_keys=True, indent=4) - try: - resp = urllib2.urlopen("{}/new-authz".format(CA), id_data) - result = json.loads(resp.read()) - except urllib2.HTTPError as e: - sys.stderr.write("Error: id_data:\n") - sys.stderr.write(id_data) - sys.stderr.write("\n") - sys.stderr.write(e.read()) - sys.stderr.write("\n") - raise - token, uri = [[c['token'], c['uri']] for c in result['challenges'] if c['type'] == "simpleHttps"][0] + #need to perform challenges for each domain + csr_authz = [] + for n, i in enumerate(ids): + + #Step 6: Get simpleHttps challenge token + sys.stderr.write("Requesting challenges for {}...\n".format(i['domain'])) + id_data = json.dumps({ + "header": header, + "protected": i['nonce64'], + "payload": i['data64'], + "signature": i['sig64'], + }, sort_keys=True, indent=4) + try: + resp = urllib2.urlopen("{}/new-authz".format(CA), id_data) + result = json.loads(resp.read()) + except urllib2.HTTPError as e: + sys.stderr.write("Error: id_data:\n") + sys.stderr.write(id_data) + sys.stderr.write("\n") + sys.stderr.write(e.read()) + sys.stderr.write("\n") + raise + token, uri = [[c['token'], c['uri']] for c in result['challenges'] if c['type'] == "simpleHttp"][0] + csr_authz.append(re.search("^([^?]+)", uri).group(1)) - #Step 7: Ask the user to host the token on their server - sys.stderr.write(""" -STEP 2: You need to run these two commands on {} (don't stop the python command). + #Step 7: Ask the user to host the token on their server + sys.stderr.write(""" +STEP {}: You need to run this command on {} (don't stop the python command until the next step). -openssl req -new -newkey rsa:2048 -days 365 -subj "/CN=a" -nodes -x509 -keyout a.key -out a.crt -sudo python -c "import BaseHTTPServer, ssl; \\ +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('{}'); \\ - s = BaseHTTPServer.HTTPServer(('0.0.0.0', 443), h); \\ - s.socket = ssl.wrap_socket(s.socket, keyfile='a.key', certfile='a.crt'); \\ + s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \\ s.serve_forever()" -""".format(domain, token, token)) +""".format(n+2, i['domain'], token)) - stdout = sys.stdout - sys.stdout = sys.stderr - raw_input("Press Enter when you've got the python command running on your server...".format(domain)) - sys.stdout = stdout + stdout = sys.stdout + sys.stdout = sys.stderr + raw_input("Press Enter when you've got the python command running on your server...") + sys.stdout = stdout - #Step 8: Let the CA know you're ready for the challenge - sys.stderr.write("Requesting verification...\n") - test_data = json.dumps({ - "header": header, - "protected": test_nonce64, - "payload": test_b64, - "signature": test_sig64, - }, sort_keys=True, indent=4) - try: - resp = urllib2.urlopen(uri, test_data) - test_result = json.loads(resp.read()) - except urllib2.HTTPError as e: - sys.stderr.write("Error: test_data:\n") - sys.stderr.write(test_data) - sys.stderr.write("\n") - sys.stderr.write(e.read()) - sys.stderr.write("\n") - raise + #Step 8: Let the CA know you're ready for the challenge + sys.stderr.write("Requesting verification for {}...\n".format(i['domain'])) + test_data = json.dumps({ + "header": header, + "protected": tests[n]['nonce64'], + "payload": tests[n]['data64'], + "signature": tests[n]['sig64'], + }, sort_keys=True, indent=4) + try: + resp = urllib2.urlopen(uri, test_data) + test_result = json.loads(resp.read()) + except urllib2.HTTPError as e: + sys.stderr.write("Error: test_data:\n") + sys.stderr.write(test_data) + sys.stderr.write("\n") + sys.stderr.write(e.read()) + sys.stderr.write("\n") + raise #Step 9: Build the certificate request payload 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_authz = re.search("^([^?]+)", uri).groups()[0] csr_raw = json.dumps({ "csr": csr_der64, - "authorizations": [csr_authz], + "authorizations": csr_authz, }, sort_keys=True, indent=4) csr_b64 = _b64(csr_raw) try: @@ -254,7 +289,7 @@ sudo python -c "import BaseHTTPServer, ssl; \\ #Step 10: Ask the user to sign the certificate request sys.stderr.write(""" -STEP 3: You need to sign one more file (replace 'user.key' with your user private key). +FINAL STEP: You need to sign one more file (replace 'user.key' with your user private key). openssl dgst -sha256 -sign user.key -out {} {} -- cgit v1.2.3