summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Roesler <diafygi@gmail.com>2015-06-14 10:32:01 -0700
committerDaniel Roesler <diafygi@gmail.com>2015-06-14 10:32:01 -0700
commit3b1ab81b0f58ebaf66ed3f0fa1a49ff53ed20b40 (patch)
tree80e965d503fb424ff6b15d321eef4af1a4f4b646
parentc45a83810ee3972925d95b2ce8d6b466e5ccbacd (diff)
fixed #3, added support for multiple domains via subject alt names. also moved to SimpleHTTP with tls off
-rw-r--r--README.md58
-rw-r--r--sign_csr.py235
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 {} {}