summaryrefslogtreecommitdiffstats
path: root/revoke_crt.py
blob: 22d26606bd21a18e2f9e997f8e9aa626feb4e8cd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
import argparse, subprocess, json, os, urllib.request, sys, base64, binascii, copy, \
    tempfile, re

from urllib.request import urlopen
from urllib.error import HTTPError

def revoke_crt(pubkey, crt):
    """Use the ACME protocol to revoke an ssl certificate signed by a
    certificate authority.

    :param string pubkey: Path to the user account public key.
    :param string crt: Path to the signed certificate.
    """
    #CA = "https://acme-staging.api.letsencrypt.org"
    CA = "https://acme-v01.api.letsencrypt.org"
    nonce_req = urllib.request.Request("{0}/directory".format(CA))
    nonce_req.get_method = lambda : 'HEAD'

    def _b64(b):
        "Shortcut function to go from bytes to jwt base64 string"
        if type(b) is str:
            b = b.encode()

        return base64.urlsafe_b64encode(b).decode().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, universal_newlines=True)
    out, err = proc.communicate()
    if proc.returncode != 0:
        raise IOError("Error loading {0}".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{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": 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("{0}.{1}".format(crt_protected64, crt_b64).encode())
    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 a file (replace 'user.key' with your user private key)

openssl dgst -sha256 -sign user.key -out {0} {1}

""".format(crt_file_sig_name, crt_file_name))

    temp_stdout = sys.stdout
    sys.stdout = sys.stderr
    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 = urlopen("{0}/acme/revoke-cert".format(CA), crt_data.encode())
        signed_der = resp.read()
    except HTTPError as e:
        sys.stderr.write("Error: crt_data:\n")
        sys.stderr.write(crt_data)
        sys.stderr.write("\n")
        sys.stderr.write(e.read().decode())
        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 PUBLIC 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)