forked from mozilla/cipherscan
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathparse_CAs.py
333 lines (267 loc) · 10.8 KB
/
parse_CAs.py
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# Author: Hubert Kario - 2014
from __future__ import division, print_function
path = "./results/"
ca_certs_path = "./ca_files"
certs_path = "./certs"
""" only root CAs, no cached intermediate certs """
trust_path = "./ca_trusted"
import json
import sys
from collections import defaultdict
import os
from OpenSSL import crypto
from operator import itemgetter
invocations = defaultdict(int)
total = 0
hosts = 0
chains = defaultdict(int)
chain_len = defaultdict(int)
keysize = defaultdict(int)
keysize_per_chain = defaultdict(int)
root_CA = defaultdict(int)
sig_alg = defaultdict(int)
intermediate_CA = defaultdict(int)
effective_security = defaultdict(int)
subject_hashes = {}
issuer_hashes = {}
def get_cert_subject_name(cert):
subject = cert.get_subject()
commonName = None
organization = None
for elem,val in subject.get_components():
if elem == "CN" and commonName is None:
commonName = val
if elem == "O" and organization is None:
organization = val
s_hash = "(" + ("%0.8X" % subject.hash()).lower() + ") "
if commonName is not None:
return s_hash + commonName
elif organization is not None:
return s_hash + organization
else:
return s_hash
def get_path_for_hash(cert_hash):
f_name = certs_path + '/' + cert_hash + '.pem'
if not os.path.exists(f_name):
f_name = ca_certs_path + '/' + cert_hash + '.pem'
if not os.path.exists(f_name):
sys.stderr.write("File with hash {0} ({1}) is missing!\n".format(
cert_hash, f_name))
return None
return f_name
""" convert RSA and DSA key sizes to estimated Level of Security """
def rsa_key_size_to_los(size):
if size < 760:
return 40
elif size < 1020:
return 64
elif size < 2040:
return 80
elif size < 3068:
return 112
elif size < 4094:
return 128
elif size < 7660:
return 152
elif size < 15300:
return 192
else:
return 256
""" convert signature algotihm to estimated Level of Security """
def sig_alg_to_los(name):
if 'MD5' in name.upper():
return 64
elif 'SHA1' in name.upper():
return 80
elif 'SHA224' in name.upper():
return 112
elif 'SHA256' in name.upper():
return 128
elif 'SHA384' in name.upper():
return 192
elif 'SHA512' in name.upper():
return 256
else:
raise UnknownSigAlgError
def collect_key_sizes(file_names):
tmp_keysize = {}
""" don't collect signature alg for the self signed root """
with open(file_names[-1]) as cert_file:
cert_pem = cert_file.read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
pubkey = cert.get_pubkey()
if pubkey.type() == crypto.TYPE_RSA:
keysize['RSA ' + str(pubkey.bits())] += 1
tmp_keysize['RSA ' + str(pubkey.bits())] = 1
security_level = rsa_key_size_to_los(pubkey.bits())
elif pubkey.type() == crypto.TYPE_DSA:
keysize['DSA ' + str(pubkey.bits())] += 1
tmp_keysize['DSA ' + str(pubkey.bits())] = 1
security_level = rsa_key_size_to_los(pubkey.bits())
# following 408 should be crypto.TYPE_ECDSA, but even new(ish) version
# of OpenSSL Python module don't define it
elif pubkey.type() == 408:
keysize['ECDSA ' + str(pubkey.bits())] += 1
tmp_keysize['ECDSA ' + str(pubkey.bits())] = 1
security_level = pubkey.bits()/2
else:
keysize[str(pubkey.type()) + ' ' + str(pubkey.bits())] += 1
security_level = 0
root_CA[get_cert_subject_name(cert)] += 1
""" exclude the self signed root and server cert from stats """
for f_name in file_names[1:-1]:
with open(f_name) as cert_file:
cert_pem = cert_file.read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
pubkey = cert.get_pubkey()
if pubkey.type() == crypto.TYPE_RSA:
keysize['RSA ' + str(pubkey.bits())] += 1
tmp_keysize['RSA ' + str(pubkey.bits())] = 1
c_key_level = rsa_key_size_to_los(pubkey.bits())
elif pubkey.type() == crypto.TYPE_DSA:
keysize['DSA ' + str(pubkey.bits())] += 1
tmp_keysize['DSA ' + str(pubkey.bits())] = 1
c_key_level = rsa_key_size_to_los(pubkey.bits())
elif pubkey.type() == 408:
keysize['ECDSA ' + str(pubkey.bits())] += 1
tmp_keysize['ECDSA ' + str(pubkey.bits())] = 1
c_key_level = pubkey.bits() / 2
else:
keysize[str(pubkey.type()) + ' ' + str(pubkey.bits())] += 1
c_key_level = 0
if security_level > c_key_level:
security_level = c_key_level
sig_alg[cert.get_signature_algorithm()] += 1
c_sig_level = sig_alg_to_los(cert.get_signature_algorithm())
if security_level > c_sig_level:
security_level = c_sig_level
intermediate_CA[get_cert_subject_name(cert)] += 1
for key_s in tmp_keysize:
keysize_per_chain[key_s] += 1
# XXX doesn't handle the situation in which the CA uses its certificate
# for a web server properly
with open(file_names[0]) as cert_file:
cert_pem = cert_file.read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
pubkey = cert.get_pubkey()
if pubkey.type() == crypto.TYPE_RSA:
c_key_level = rsa_key_size_to_los(pubkey.bits())
elif pubkey.type() == crypto.TYPE_DSA:
c_key_level = rsa_key_size_to_los(pubkey.bits())
elif pubkey.type() == 408:
c_key_level = pubkey.bits() / 2
else:
c_key_level = 0
if security_level > c_key_level:
security_level = c_key_level
c_sig_level = sig_alg_to_los(cert.get_signature_algorithm())
if security_level > c_sig_level:
security_level = c_sig_level
effective_security[security_level] += 1
with open("parsed") as res_file:
for line in res_file:
try:
res = json.loads(line)
except ValueError as e:
print("can't process line: " + line)
continue
f=res
try:
server_chain_trusted = False
server_chain_complete = False
server_chains = []
valid = False
""" Keep certificates in memory for a given file """
known_certs = {}
if not "chains" in f:
continue
results = f["chains"]
""" discard hosts with empty results """
if len(results) < 1:
continue
""" loop over list of ciphers """
for entry in results:
""" skip invalid results """
if not 'chain' in entry:
continue
valid = True
if entry['chain'] == "untrusted":
continue
if entry['chain'] == "complete":
server_chain_complete = True
server_chain_trusted = True
if entry['chain'] == "incomplete":
server_chain_trusted = True
server_chains += [entry['certificates']]
if server_chain_trusted:
if server_chain_complete:
chains["complete"] += 1
print("complete: " + f['host'])
else:
chains["incomplete"] += 1
print("incomplete: " + f['host'])
else:
chains["untrusted"] += 1
print("untrusted: " + f['host'])
if valid:
hosts += 1
for chain in server_chains:
f_names = []
for hash in chain:
path = get_path_for_hash(hash)
f_names += [path]
collect_key_sizes(f_names)
chain_len[str(len(chain))] += 1
if len(chain) == 1:
sys.stderr.write("file with chain 1 long: " + line)
total += 1
except TypeError as e:
sys.stderr.write("can't process: " + line)
continue
""" Display stats """
#print("openssl invocations: " + str(invocations["openssl"]))
print("Statistics from " + str(total) + " chains provided by " + str(hosts) + " hosts")
print("\nServer provided chains Count Percent")
print("-------------------------+---------+-------")
for stat in sorted(chains):
percent = round(chains[stat] / hosts * 100, 4)
sys.stdout.write(stat.ljust(25) + " " + str(chains[stat]).ljust(10) + str(percent).ljust(4) + "\n")
print("\nTrusted chain statistics")
print("========================")
print("\nChain length Count Percent")
print("-------------------------+---------+-------")
for stat in sorted(chain_len):
percent = round(chain_len[stat] / total * 100, 4)
sys.stdout.write(stat.ljust(25) + " " + str(chain_len[stat]).ljust(10) + str(percent).ljust(4) + "\n")
print("\nCA key size in chains Count")
print("-------------------------+---------")
for stat in sorted(keysize):
sys.stdout.write(stat.ljust(25) + " " + str(keysize[stat]).ljust(10) + "\n")
print("\nChains with CA key Count Percent")
print("-------------------------+---------+-------")
for stat in sorted(keysize_per_chain):
percent = round(keysize_per_chain[stat] / total * 100, 4)
sys.stdout.write(stat.ljust(25) + " " + str(keysize_per_chain[stat]).ljust(10) + str(percent).ljust(4) + "\n")
print("\nSignature algorithm (ex. root) Count")
print("------------------------------+---------")
for stat in sorted(sig_alg):
sys.stdout.write(stat.ljust(30) + " " + str(sig_alg[stat]).ljust(10) + "\n")
print("\nEff. host cert chain LoS Count Percent")
print("-------------------------+---------+-------")
for stat in sorted(effective_security):
percent = round(effective_security[stat] / total * 100, 4)
sys.stdout.write(str(stat).ljust(25) + " " + str(effective_security[stat]).ljust(10) + str(percent).ljust(4) + "\n")
print("\nRoot CAs Count Percent")
print("---------------------------------------------+---------+-------")
for stat, val in sorted(root_CA.items(), key=itemgetter(1), reverse=True):
percent = round(val / total * 100, 4)
sys.stdout.write(stat.ljust(45)[0:45] + " " + str(val).ljust(10) + str(percent).ljust(4) + "\n")
print("\nIntermediate CA Count Percent")
print("---------------------------------------------+---------+-------")
for stat, val in sorted(intermediate_CA.items(), key=itemgetter(1), reverse=True):
percent = round(val / total * 100, 4)
sys.stdout.write(stat.ljust(45)[0:45] + " " + str(val).ljust(10) + str(percent).ljust(4) + "\n")