diff options
| author | Jason Heard <jasonpheard@gmail.com> | 2015-11-15 15:38:57 -0700 | 
|---|---|---|
| committer | Jason Heard <jasonpheard@gmail.com> | 2015-11-15 15:38:57 -0700 | 
| commit | defce7e02af5545f7977e6bd94f605d06584a6e7 (patch) | |
| tree | 767b8e49a12a9aa77b1e3d02d3e3f44b6fb82902 | |
| parent | 2c635c0df0f2e174b67cfcfddac425863e92e98a (diff) | |
Add revocation script
| -rw-r--r-- | revoke_crt.py | 136 | 
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) + | 
