summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Roesler <diafygi@gmail.com>2015-06-10 18:12:24 -0700
committerDaniel Roesler <diafygi@gmail.com>2015-06-10 18:12:24 -0700
commitf496e5d45ff9e953d0b1421611b4864e40e2530f (patch)
tree01ed081bf5a9bf1e49db7a8b419e07d62bee95f2
parent9607deb7b259499725ce1c705a1664199f51881d (diff)
updated to new registration flow for ACME
-rw-r--r--README.md2
-rw-r--r--sign_csr.py345
2 files changed, 147 insertions, 200 deletions
diff --git a/README.md b/README.md
index 7152cae..19f9d44 100644
--- a/README.md
+++ b/README.md
@@ -91,7 +91,7 @@ Prerequisites:
Example: Generate a key, create a csr, and have it signed.
--------------
$ openssl genrsa -out priv.key 4096
-$ openssl req -new -sha256 -key priv.key -out cert.csr
+$ openssl req -new -subj "/CN=example.com" -key priv.key -out cert.csr
$ python sign_csr.py cert.csr > signed.crt
--------------
diff --git a/sign_csr.py b/sign_csr.py
index 6e759fa..ccf102e 100644
--- a/sign_csr.py
+++ b/sign_csr.py
@@ -11,7 +11,7 @@ def sign_csr(csr):
:rtype: string
"""
- CA = "https://letsencrypt-demo.org/"
+ CA = "https://www.letsencrypt-demo.org/acme"
#CA = "http://localhost:8888/"
def _b64(b):
@@ -28,7 +28,7 @@ def sign_csr(csr):
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
domain, pub_hex, pub_exp = re.search("\
-Subject:.+? CN=([^\s,;/]+).+?\
+Subject:.*? CN=([^\s,;/]+).*?\
Modulus\:\s+00:([a-f0-9\:\s]+?)\
Exponent\: ([0-9]+)\
", out, re.MULTILINE|re.DOTALL).groups()
@@ -39,228 +39,175 @@ Exponent\: ([0-9]+)\
pub_exp = "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,
+ },
+ }
+ header_json = json.dumps(header, sort_keys=True)
+ header64 = _b64(header_json)
sys.stderr.write("Found domain '{}'\n".format(domain))
- #Step 2: Get challenges from the CA
- sys.stderr.write("Requesting challenges...")
- data = json.dumps({
- "type": "challengeRequest",
- "identifier": domain,
- })
- resp = urllib2.urlopen(CA, data)
- result = resp.read()
- resp_json = json.loads(result)
- sessionID = resp_json['sessionID']
- nonce64 = resp_json['nonce']
- nonce = _a64(nonce64)
- challenges = resp_json['challenges']
- sys.stderr.write("Challenges received!\n")
-
- #Step 3: Get the dvsni challenge
- sys.stderr.write("Parsing dvsni challenge...\n")
- challenge = [c for c in challenges if c['type'] == "dvsni"][0]
- dvsni_nonce = binascii.unhexlify(challenge['nonce'])
- dvsni_r = _a64(challenge['r'])
-
- #Step 4: Calculate the sni code
- client_code = os.urandom(32)
- sni_code = hashlib.sha256(dvsni_r + client_code).hexdigest()
- sni = sni_code + ".acme.invalid"
-
- #Step 5: Generate the OpenSSL configuration for the SNI challenge
- sys.stderr.write("Generating test configuation...\n")
- proc = subprocess.Popen(["openssl", "version", "-d"],
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- out, err = proc.communicate()
- openssl_dir = re.search("OPENSSLDIR: \"([^\"]+)\"", out).group(1)
- openssl_cnf = open(os.path.join(openssl_dir, "openssl.cnf")).read()
- test_conf = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".cnf")
- test_conf.write(openssl_cnf)
- test_conf.write("\n\n[ alt_names ]\nsubjectAltName = DNS:" + sni + "\n")
- test_conf.flush()
-
- #Step 6: Generate a self-signed ssl cert with the sni altname
- sys.stderr.write("Generating test key for dvsni challenge...\n")
- test_crt = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".crt")
- test_crt_name = os.path.basename(test_crt.name)
- test_pem = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".pem")
- test_pem_name = os.path.basename(test_pem.name)
- proc = subprocess.Popen(
- ["openssl", "req", "-new", "-newkey", "rsa:4096",
- "-days", "365", "-nodes", "-x509", "-sha256",
- "-subj", "/C=AA/ST=A/L=A/O=A/CN={}".format(domain),
- "-config", test_conf.name, "-extensions", "alt_names",
- "-keyout", test_pem.name, "-out", test_crt.name],
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- proc.wait()
- sys.stderr.write("Test key generated!\n")
-
- #Step 7: Get ready to sign the challenge response
- msgsig_nonce = os.urandom(16)
- msgsig_nonce64 = _b64(msgsig_nonce)
- msg = msgsig_nonce + domain + nonce
- msg_file = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".msg")
- msg_file_name = os.path.basename(msg_file.name)
- msg_file.write(msg)
- msg_file.flush()
- msgsig_file = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".msgsig")
- msgsig_file_name = os.path.basename(msgsig_file.name)
-
- #Step 8: Get ready to sign the certificate request response
- dersig_nonce = os.urandom(16)
- dersig_nonce64 = _b64(dersig_nonce)
+ #Step 2: Generate the payloads that need to be signed
+ #registration
+ reg_raw = json.dumps({
+ "contact": ["mailto:webmaster@{}".format(domain)],
+ "agreement": "https://www.letsencrypt-demo.org/terms",
+ }, sort_keys=True)
+ reg_b64 = _b64(reg_raw)
+ reg_file = tempfile.NamedTemporaryFile(dir=".", prefix="letsencrypt_reg_", suffix=".json")
+ reg_file.write("{}.{}".format(header64, reg_b64))
+ reg_file.flush()
+ reg_file_name = os.path.basename(reg_file.name)
+ reg_file_sig = reg_file_name.replace(".json", ".sig")
+
+ #identifier
+ id_raw = json.dumps({"identifier": {"type": "dns", "value": domain}}, sort_keys=True)
+ id_b64 = _b64(id_raw)
+ id_file = tempfile.NamedTemporaryFile(dir=".", prefix="letsencrypt_id_", suffix=".json")
+ id_file.write("{}.{}".format(header64, id_b64))
+ id_file.flush()
+ id_file_name = os.path.basename(id_file.name)
+ id_file_sig = id_file_name.replace(".json", ".sig")
+
+ #challenge
+ test_path = _b64(os.urandom(16))
+ test_raw = json.dumps({"type": "simpleHttps", "path": test_path}, sort_keys=True)
+ test_b64 = _b64(test_raw)
+ test_file = tempfile.NamedTemporaryFile(dir=".", prefix="letsencrypt_test_", suffix=".json")
+ test_file.write("{}.{}".format(header64, test_b64))
+ test_file.flush()
+ test_file_name = os.path.basename(test_file.name)
+ test_file_sig = test_file_name.replace(".json", ".sig")
+
+ #certificate
proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- der_raw, err = proc.communicate()
- der = dersig_nonce + der_raw
- der_file = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".der")
- der_file_name = os.path.basename(der_file.name)
- der_file.write(der)
- der_file.flush()
- dersig_file = tempfile.NamedTemporaryFile(dir=".", prefix="test_", suffix=".dersig")
- dersig_file_name = os.path.basename(dersig_file.name)
-
- #Step 9: Get the user to sign the request and start the https server
+ csr_der, err = proc.communicate()
+ csr_der64 = _b64(csr_der)
+ csr_raw = json.dumps({"csr": csr_der64}, sort_keys=True)
+ csr_b64 = _b64(csr_raw)
+ csr_file = tempfile.NamedTemporaryFile(dir=".", prefix="letsencrypt_csr_", suffix=".json")
+ csr_file.write("{}.{}".format(header64, csr_b64))
+ csr_file.flush()
+ csr_file_name = os.path.basename(csr_file.name)
+ csr_file_sig = csr_file_name.replace(".json", ".sig")
+
+ #Step 3: Ask the user to sign the payloads
sys.stderr.write("""
-====================================================
-================USER ACTION REQUIRED================
-====================================================
+==================================================
+================STEP 1: SIGN FILES================
+==================================================
Since we don't ask for your private key or sudo access, you will need
to do some manual commands in order to get a signed certificate. Here's
-what you need to do:
+the first set of commands (do them in a new terminal window).
-1. Sign some files requested by the certificate authority.
-2. Copy a test key and certificate to your server.
-3. Run an https server with the test key and certificate.
+1. Sign the four request files we've generated (commands below).
-We've listed the commands you need to do this below. You should be able
-to copy and paste them into a new terminal window.
-
-(NOTE: Replace 'priv.key' below with your private key, if different)
-(NOTE: Replace 'ubuntu' below with your sudo user, if different)
-
-COMMANDS:
---------------------------
-#Step 1: Sign the needed files
+openssl dgst -sha256 -sign priv.key -out {} {}
+openssl dgst -sha256 -sign priv.key -out {} {}
openssl dgst -sha256 -sign priv.key -out {} {}
openssl dgst -sha256 -sign priv.key -out {} {}
-#Step 2: Copy the test key and certificate to your server
-scp {} ubuntu@{}:{}
-scp {} ubuntu@{}:{}
-
-#Step 3: Run an https server with the test key and certificate
-ssh -t ubuntu@{} "sudo python -c \\"import BaseHTTPServer, ssl; \\
- httpd = BaseHTTPServer.HTTPServer(('0.0.0.0', 443), BaseHTTPServer.BaseHTTPRequestHandler); \\
- httpd.socket = ssl.wrap_socket(httpd.socket, keyfile='{}', certfile='{}'); \\
- httpd.serve_forever()\\""
---------------------------
+(NOTE: Replace 'priv.key' below with your private key, if different)
-====================================================
-====================================================
-====================================================
+==================================================
+==================================================
+==================================================
""".format(
- msgsig_file_name, msg_file_name,
- dersig_file_name, der_file_name,
- test_pem_name, domain, test_pem_name,
- test_crt_name, domain, test_crt_name,
- domain, test_pem_name, test_crt_name))
+ reg_file_sig, reg_file_name,
+ id_file_sig, id_file_name,
+ test_file_sig, test_file_name,
+ csr_file_sig, csr_file_name))
stdout = sys.stdout
sys.stdout = sys.stderr
raw_input("Press Enter when you've run the above commands in a new terminal window...")
sys.stdout = stdout
- #Step 10: Let the CA know you are ready for the challenge
- msgsig_file.seek(0)
- msgsig_in = msgsig_file.read()
- msgsig64 = _b64(msgsig_in)
- sys.stderr.write("Sending challenge response...\n")
- data = json.dumps({
- "type": "authorizationRequest",
- "sessionID": sessionID,
- "nonce": nonce64,
- "signature": {
- "nonce": msgsig_nonce64,
- "alg": "RS256",
- "jwk": {
- "kty": "RSA",
- "e": pub_exp64,
- "n": pub_mod64
- },
- "sig": msgsig64
- },
- "responses": [
- {
- "type": "dvsni",
- "s": _b64(client_code)
- }
- ]
- })
- resp = urllib2.urlopen(CA, data)
+ #Step 4: Load the signatures
+ reg_sig64 = _b64(open(reg_file_sig).read())
+ id_sig64 = _b64(open(id_file_sig).read())
+ test_sig64 = _b64(open(test_file_sig).read())
+ csr_sig64 = _b64(open(csr_file_sig).read())
+
+ #Step 5: Register the user
+ sys.stderr.write("Registering...")
+ reg_data = "{}.{}.{}".format(header64, reg_b64, reg_sig64)
+ resp = urllib2.urlopen("{}/new-reg".format(CA), reg_data)
result = json.loads(resp.read())
- #Step 11: Wait for the response giving authorization
- is_done = False
- while not is_done:
- if result['type'] == "defer":
- sys.stderr.write("Deferred...\n")
- time.sleep(result.get("interval", 3))
- data = json.dumps({
- "type": "statusRequest",
- "token": result['token']
- })
- resp = urllib2.urlopen(CA, data)
- result = json.loads(resp.read())
- else:
- sys.stderr.write("Sending certificate request...\n")
- is_done = True
-
- #Step 12: Request the signed certificate
- 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)
- dersig_file.seek(0)
- dersig = dersig_file.read()
- dersig64 = _b64(dersig)
- data = json.dumps({
- "type": "certificateRequest",
- "csr": csr_der64,
- "signature": {
- "alg": "RS256",
- "nonce": dersig_nonce64,
- "sig": dersig64,
- "jwk": {
- "kty": "RSA",
- "e": pub_exp64,
- "n": pub_mod64
- }
- }
- })
- resp = urllib2.urlopen(CA, data)
+ #Step 6: Get simpleHttps challenge token
+ sys.stderr.write("Requesting challenges...")
+ id_data = "{}.{}.{}".format(header64, id_b64, id_sig64)
+ resp = urllib2.urlopen("{}/new-authz".format(CA), id_data)
+ result = json.loads(resp.read())
+ token, uri = [[c['token'], c['uri']] for c in result['challenges'] if c['type'] == "simpleHttps"][0]
+
+ #Step 7: Ask the user to host the token on their server
+ sys.stderr.write("""
+=====================================================
+================STEP 2: VERIFY SERVER================
+=====================================================
+
+The certificate authority wants to verify you control the domain
+by serving a random string at a specific url.
+
+Here's the commands (RUN THESE ANYWHERE ON YOUR SERVER):
+------------
+#Create a temporary self-signed cert
+openssl req -new -newkey rsa:2048 -days 365 -subj "/CN=a" -nodes -x509 -keyout a.key -out a.crt
+
+#Serve the token on port 443
+sudo python -c "import BaseHTTPServer, ssl; \\
+ 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.serve_forever()"
+------------
+
+ALTERNATIVELY:
+If you are already serving content over https, you can add the
+token to your server config. A request to
+https://{}/.well-known/acme-challenge/{}
+should return the token "{}"
+as the body.
+TODO: apache and nginx configs showing how to do this.
+
+=====================================================
+=====================================================
+=====================================================
+
+""".format(token, domain, test_path, token))
+
+ stdout = sys.stdout
+ sys.stdout = sys.stderr
+ raw_input("Press Enter when you're serving the token on your server...")
+ sys.stdout = stdout
+
+ #Step 8: Let the CA know you're ready for the challenge
+ sys.stderr.write("Requesting verification...")
+ test_data = "{}.{}.{}".format(header64, test_b64, test_sig64)
+ resp = urllib2.urlopen(uri, test_data)
result = json.loads(resp.read())
- sys.stderr.write("Exporting signed certificate...\n")
-
- #Step 13: Parse the signed certificate
- crt_start = "-----BEGIN CERTIFICATE-----\n"
- crt_end = "\n-----END CERTIFICATE-----\n"
- crt64 = result['certificate']
- crt_pem = base64.b64encode(_a64(crt64))
- crt_pem = "\n".join(crt_pem[i:i+64] for i in xrange(0, len(crt_pem), 64))
- crt_pem = crt_start + crt_pem + crt_end
-
- #Step 14: Parse any chained certificates
- for chain64 in result.get("chains", []):
- chain_pem = base64.b64encode(_a64(chain64))
- chain_pem = "\n".join(chain_pem[i:i+64] for i in xrange(0, len(chain_pem), 64))
- chain_pem = crt_start + chain_pem + crt_end
- crt_pem += chain_pem
- sys.stderr.write("Done! Your certificate is signed by the certificate authority!\n")
-
- #Step 15: Output the chained, signed certificate!
+
+ #Step 9: Wait for a little while to let the challenge pass
+ sys.stderr.write("Waiting for verification...")
+ time.sleep(3)
+
+ #Step 10: Get the certificate signed
+ sys.stderr.write("Requesting signature...")
+ csr_data = "{}.{}.{}".format(header64, csr_b64, csr_sig64)
+ print "csr_data", csr_data
+ resp = urllib2.urlopen("{}/new-cert".format(CA), csr_data)
+ result = json.loads(resp.read())
+
return crt_pem