summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Heard <jasonpheard@gmail.com>2015-11-15 15:38:57 -0700
committerJason Heard <jasonpheard@gmail.com>2015-11-15 15:38:57 -0700
commitdefce7e02af5545f7977e6bd94f605d06584a6e7 (patch)
tree767b8e49a12a9aa77b1e3d02d3e3f44b6fb82902
parent2c635c0df0f2e174b67cfcfddac425863e92e98a (diff)
Add revocation script
-rw-r--r--revoke_crt.py136
1 files changed, 136 insertions, 0 deletions
diff --git a/revoke_crt.py b/revoke_crt.py
new file mode 100644
index 0000000..b7aa5e1
--- /dev/null
+++ b/revoke_crt.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+import argparse, subprocess, json, os, urllib2, sys, base64, binascii, ssl, \
+ hashlib, tempfile, re, time, copy, textwrap, copy
+
+
+def revoke_crt(pubkey, crt):
+ """Use the ACME protocol to revoke an ssl certificate signed by a
+ certificate authority.
+
+ :param string crt: Path to the signed certificate.
+ """
+ #CA = "https://acme-staging.api.letsencrypt.org"
+ CA = "https://acme-v01.api.letsencrypt.org"
+ TERMS = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
+ nonce_req = urllib2.Request("{}/directory".format(CA))
+ nonce_req.get_method = lambda : 'HEAD'
+
+ def _b64(b):
+ "Shortcut function to go from bytes to jwt base64 string"
+ return base64.urlsafe_b64encode(b).replace("=", "")
+
+ def _a64(a):
+ "Shortcut function to go from jwt base64 string to bytes"
+ return base64.urlsafe_b64decode(str(a + ("=" * (len(a) % 4))))
+
+ # 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)
+ out, err = proc.communicate()
+ if proc.returncode != 0:
+ raise IOError("Error loading {}".format(pubkey))
+ pub_hex, pub_exp = re.search("Modulus\:\s+00:([a-f0-9\:\s]+?)Exponent\: ([0-9]+)", out, re.MULTILINE|re.DOTALL).groups()
+ pub_mod = binascii.unhexlify(re.sub("(\s|:)", "", pub_hex))
+ pub_mod64 = _b64(pub_mod)
+ pub_exp = int(pub_exp)
+ pub_exp = "{0:x}".format(pub_exp)
+ 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,
+ },
+ }
+ sys.stderr.write("Found public key!\n".format(header))
+
+ # Step 2: Generate the payload that needs to be signed
+ # revokation request
+ proc = subprocess.Popen(["openssl", "x509", "-in", crt, "-outform", "DER"],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ crt_der, err = proc.communicate()
+ crt_der64 = _b64(crt_der)
+ crt_raw = json.dumps({
+ "resource": "revoke-cert",
+ "certificate": crt_der64,
+ }, sort_keys=True, indent=4)
+ crt_b64 = _b64(crt_raw)
+ crt_protected = copy.deepcopy(header)
+ crt_protected.update({"nonce": urllib2.urlopen(nonce_req).headers['Replay-Nonce']})
+ crt_protected64 = _b64(json.dumps(crt_protected, sort_keys=True, indent=4))
+ crt_file = tempfile.NamedTemporaryFile(dir=".", prefix="revoke_", suffix=".json")
+ crt_file.write("{}.{}".format(crt_protected64, crt_b64))
+ crt_file.flush()
+ crt_file_name = os.path.basename(crt_file.name)
+ crt_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="revoke_", suffix=".sig")
+ crt_file_sig_name = os.path.basename(crt_file_sig.name)
+
+ # Step 3: Ask the user to sign the revocation request
+ 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 {} {}
+
+""".format(crt_file_sig_name, crt_file_name))
+
+ temp_stdout = sys.stdout
+ sys.stdout = sys.stderr
+ raw_input("Press Enter when you've run the above command in a new terminal window...")
+ sys.stdout = temp_stdout
+
+ # Step 4: Load the signature and send the revokation request
+ sys.stderr.write("Requesting revocation...\n")
+ crt_file_sig.seek(0)
+ crt_sig64 = _b64(crt_file_sig.read())
+ crt_data = json.dumps({
+ "header": header,
+ "protected": crt_protected64,
+ "payload": crt_b64,
+ "signature": crt_sig64,
+ }, sort_keys=True, indent=4)
+ try:
+ resp = urllib2.urlopen("{}/acme/revoke-cert".format(CA), crt_data)
+ signed_der = resp.read()
+ except urllib2.HTTPError as e:
+ sys.stderr.write("Error: crt_data:\n")
+ sys.stderr.write(crt_data)
+ sys.stderr.write("\n")
+ sys.stderr.write(e.read())
+ sys.stderr.write("\n")
+ raise
+ sys.stderr.write("Certificate revoked!\n")
+
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description="""\
+Get a SSL certificate revoked by a Let's Encrypt (ACME) certificate authority.
+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, which gives you a chance to review the commands instead of
+trusting this script.
+
+NOTE: YOUR ACCOUNT KEY NEEDS TO BE THE SAME KEY USED TO ISSUE THE CERTIFICATE.
+
+Prerequisites:
+* openssl
+* python
+
+Example:
+--------------
+$ python revoke_crt.py --public-key user.pub domain.crt
+--------------
+
+""")
+ parser.add_argument("-p", "--public-key", required=True, help="path to your account public key")
+ parser.add_argument("crt_path", help="path to your signed certificate")
+
+ args = parser.parse_args()
+ revoke_crt(args.public_key, args.crt_path)
+