Merge pull request #384 from stablestud/master
[user-js.git] / cas.sh
blob215a95a9992a4781298f40eed6b4dfe543841568
1 #!/bin/bash
3 # life_without_ca.sh -- a script that creates a very limited set of root CAs to your firefox's cert8.db
5 # before you use this script, i suggest you run certpatrol for something like half a year to get enough certificate "samples"
7 # here's roughly what this script does:
9 # - build index of CAs from operating systems global cert store (${CERT_PATH})
10 # - build index of (intermediate) CAs from firefox's cert8.db cert store
11 # - get issuers from all the certificates seen from certificate patrol's CertPatrol.sqlite DB
12 # - try to locate the CA from the global index
13 # - if that doesn't work out (when the issuer is an intermediate CA), try to locate the intermediate CA from the cert8.db index (as firefox caches the intermediate CAs)
14 # - try to find the CA of that
15 # - construct at list of required _root_ CAs
16 # - locate those from ${CERT_PATH}
17 # - import to (new) cert8.db
18 # - note that you must have removed the libnssckbi.so cert library, so firefox doesn't use it's bloated CA store (for more info, see: https://blog.torproject.org/blog/life-without-ca)
19 # - set trusted to verify websites
20 # - profit?
22 # TODO:
23 # - add openssl verify magic
24 # - we could construct the required CAs list from only the intermediate CAs in cert8.db, although it will not be complete
25 # - use /usr/share/ca-certificates instead of ${CERT_PATH}
26 # - search all sites from certpatrol, that use CA <x>
27 # - events.ccc.de - /usr/share/ca-certificates/cacert.org/cacert.org.crt -> should we include this? maybe as untrusted?
28 # - crawl Firefox history for HTTPS sites and get certs?
30 # domains that don't work (use some other CA):
31 # - kb.wisc.edu
33 for PROGRAM in \
34 gawk \
35 openssl \
36 certutil \
37 sqlite3
39 if ! hash "${PROGRAM}" 2>/dev/null
40 then
41 printf "[-] error: command not found in PATH: %s\n" "${PROGRAM}" >&2
42 exit 1
44 done
45 unset PROGRAM
47 # from http://wiki.bash-hackers.org/scripting/debuggingtips#making_xtrace_more_useful:
48 export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
49 declare -A CAs=()
50 declare -A CERT8_CAs=()
51 #declare -A CERT8_NICKS=()
52 # this uses all the available firefox profiles
53 INDEX_ALL_CERT8=0
54 declare -a BASIC_LIST=(
55 "mozilla/AddTrust_External_Root.crt"
56 "mozilla/Baltimore_CyberTrust_Root.crt"
57 "mozilla/COMODO_Certification_Authority.crt"
58 "mozilla/Deutsche_Telekom_Root_CA_2.crt"
59 "mozilla/DigiCert_High_Assurance_EV_Root_CA.crt"
60 "mozilla/DigiCert_Global_Root_CA.crt"
61 "mozilla/Entrust.net_Secure_Server_CA.crt"
62 "mozilla/Entrust.net_Premium_2048_Secure_Server_CA.crt"
63 "mozilla/Entrust_Root_Certification_Authority.crt"
64 "mozilla/Equifax_Secure_CA.crt"
65 "mozilla/GTE_CyberTrust_Global_Root.crt"
66 "mozilla/GeoTrust_Global_CA.crt"
67 "mozilla/GeoTrust_Primary_Certification_Authority.crt"
68 "mozilla/GeoTrust_Primary_Certification_Authority_-_G3.crt"
69 "mozilla/GlobalSign_Root_CA.crt"
70 "mozilla/Go_Daddy_Class_2_CA.crt"
71 "mozilla/Go_Daddy_Root_Certificate_Authority_-_G2.crt"
72 "mozilla/Starfield_Class_2_CA.crt"
73 "mozilla/StartCom_Certification_Authority.crt"
74 "mozilla/UTN_USERFirst_Hardware_Root_CA.crt"
75 "mozilla/ValiCert_Class_2_VA.crt"
76 "mozilla/Verisign_Class_3_Public_Primary_Certification_Authority_-_G3.crt"
77 "mozilla/VeriSign_Class_3_Public_Primary_Certification_Authority_-_G5.crt"
78 "mozilla/thawte_Primary_Root_CA.crt"
79 "mozilla/thawte_Primary_Root_CA_-_G3.crt"
80 "mozilla/SecureTrust_CA.crt"
81 "mozilla/QuoVadis_Root_CA_2.crt"
82 "mozilla/DST_Root_CA_X3.crt"
84 CERT_PATH="$( openssl version -a|grep "^OPENSSLDIR:"|cut -d'"' -f2 )/certs"
85 WRN=$'\033[1;31m'
86 RST=$'\033[0m'
87 DEBUG=0
89 function import_cas() {
90 local REQUIRED_CA
91 local NICKNAME
93 if [ -z "${FF_HOME}" ]
94 then
95 echo "[-] error: FF_HOME not defined!" 1>&2
96 return 1
99 echo -e "\nimporting CAs:"
100 # create cert8.db for the new profile
101 for REQUIRED_CA in ${REQUIRED_CAs[*]}
103 #if [ "${REQUIRED_CA:0:1}" != "/" ]
104 #then
105 # REQUIRED_CA="${CERT_PATH}/${REQUIRED_CA}"
107 if [ ! -f "${REQUIRED_CA}" ]
108 then
109 echo "[-] error: file \`${REQUIRED_CA}' not found!" 1>&2
110 continue
112 echo " ${REQUIRED_CA}"
113 # certutil requires a "nickname", so we'll use the CN or OU
114 # TODO: change this?
115 NICKNAME=$( openssl x509 -in "${REQUIRED_CA}" -noout -subject | sed 's/^.*\(CN\|OU\)=//' )
116 certutil -A -n "${NICKNAME}" -t CT,c,c -a -d "${FF_HOME}" 0<"${REQUIRED_CA}"
118 # TEST!!! allow code signing
119 #cat "${REQUIRED_CA}" | certutil -A -n "${NICKNAME}" -t CT,c,C -a -d "${FF_HOME}"
120 done
122 return
123 } # import_cas()
125 function expand_cert_path() {
126 # this function follows symlinked files, until the actual file is found.
127 local FILE="${1}"
128 if [ ! -f "${FILE}" ]
129 then
130 echo "[-] error: file \`${FILE}' not found!" 1>&2
131 return 1
133 while [ -h "${FILE}" ]
135 FILE=$( readlink "${FILE}" )
136 if [ "${FILE:0:1}" != "/" ]
137 then
138 FILE="${CERT_PATH}/${FILE}"
140 done
141 echo "${FILE}"
142 return
143 } # expand_cert_path()
145 function get_required_cas_list() {
146 local FINGERPRINT
147 local -a FINGERPRINTS
148 local REQUIRED_CA
149 local ISSUER
150 local ISSUER_CN
152 if [ -z "${CP}" ]
153 then
154 echo "[-] error: no CertPatrol path defined!" 1>&2
155 return 1
156 elif [ ! -f "${CP}" ]
157 then
158 echo "[-] error: certpatrol DB \`${CP}' not found!" 1>&2
159 return 1
162 # read all the issuer fingerprints from certificate patrol's DB
163 echo "reading issuer fingerprints from certpatrol's DB"
164 FINGERPRINTS=( $( sqlite3 "${CP}" 0<<<"select distinct issuerSha1Fingerprint from certificates where issuerSha1Fingerprint is not '';" ) )
165 echo -e "${#FINGERPRINTS[*]} issuer fingerprints found\n"
166 for FINGERPRINT in ${FINGERPRINTS[*]}
168 REQUIRED_CA=""
169 # check if the issuer cert is a root CA
170 if [ -n "${CAs[${FINGERPRINT}]}" ]
171 then
172 echo "${FINGERPRINT}: root CA found: ${CAs[${FINGERPRINT}]}"
173 REQUIRED_CA="${CAs[${FINGERPRINT}]}"
174 # TODO: openssl verify
175 # this is the most common case, as the certs are usually signed by intermediate CA
176 elif [ -n "${CERT8_CAs[${FINGERPRINT}]}" ]
177 then
178 echo "${FINGERPRINT}: found on cert8.db \"${CERT8_CAs[${FINGERPRINT}]}\""
180 # find the root CA that signed the intermediate CA
181 # TODO: it would be better to get the fingerprint of the issuer, instead of issuer_hash
182 ISSUER=$( certutil -L -n "${CERT8_CAs[${FINGERPRINT}]}" -a -d "${OLD_FF_HOME}" | openssl x509 -noout -issuer_hash )
183 # TODO: iterate through n
184 REQUIRED_CA=$( expand_cert_path "${CERT_PATH}/${ISSUER}.0" 2>/dev/null )
185 if [ -z "${REQUIRED_CA}" ]
186 then
187 # TODO: check if the cert actually is root CA
188 echo -e " [${WRN}-${RST}]WARNING: no root CA found for this cert -> continue"
189 continue
191 echo " root CA found on file system: ${REQUIRED_CA}"
193 # verify
194 #certutil -L -n "${CERT8_CAs[${FINGERPRINT}]}" -a -d "${OLD_FF_HOME}" | openssl verify -CAfile "${REQUIRED_CA}"
195 else
196 # issuer cert not found
197 if (( DEBUG ))
198 then
199 ISSUER_CN=$( sqlite3 "${CP}" 0<<<"select distinct issuerCommonName from certificates where issuerSha1Fingerprint is \"${FINGERPRINT}\";" )
200 echo -e "${FINGERPRINT}: \033[1;31mnot\033[0m found \"${ISSUER_CN}\"!" 1>&2
201 # print hosts that use this issuer
202 echo " sites that use this CA:"
203 sqlite3 "${CP}" 0<<<"select host from certificates where issuerSha1Fingerprint is \"${FINGERPRINT}\";" | sed 's/^/ /'
206 if [ -n "${REQUIRED_CA}" ]
207 then
208 REQUIRED_CAs+=( ${REQUIRED_CA} )
210 done # for FINGERPRINT
211 # UGLY HACK WARNING!
212 REQUIRED_CAs=( $( tr ' ' '\n' 0<<<"${REQUIRED_CAs[*]}" | sort -u ) )
214 return
215 } # get_required_cas_list()
217 function print_required_cas_list() {
218 local REQUIRED_CA
220 echo -e "\n\nrequired CAs (${#REQUIRED_CAs[*]}):"
221 for REQUIRED_CA in ${REQUIRED_CAs[*]}
223 echo " ${REQUIRED_CA}"
224 if [ ! -f "${REQUIRED_CA}" ]
225 then
226 echo " [-] WARNING: not found!" 1>&2
228 done
230 return
231 } # print_required_cas_list()
233 function print_countries() {
234 local -a NICKNAMES
235 local NICKNAME
236 local OIFS
237 local COUNTRY
238 OIFS=${IFS}
239 IFS=$'\n'
240 # get the "nicknames", as this is the way certutil handles the certs
241 NICKNAMES=( $( certutil -L -d "${FF_HOME}" | grep -F -v ",," | sed '1,4d' | gawk 'NF--' ) )
242 IFS=${OIFS}
243 for NICKNAME in "${NICKNAMES[@]}"
245 # print the PEM from the cert8.db and get the FP with openssl
246 COUNTRY=$( certutil -L -n "${NICKNAME}" -a -d "${FF_HOME}" | openssl x509 -noout -subject | grep -o "C=[A-Z]\+" )
247 if [ -n "${COUNTRY}" ]
248 then
249 echo "${COUNTRY#C=}"
250 else
251 echo "[-] warning: country not found for \`${NICKNAME}'!" 1>&2
253 done | sort | uniq -c
255 } # print_countries()
257 function reverse_index() {
258 # this builds an index of the cert8.db nicknames
260 local -a NICKNAMES
261 local NICKNAME
262 local OIFS
263 local -a FPS=()
264 local FP
266 if [ -z "${FF_HOME}" ]
267 then
268 echo "[-] error: FF_HOME not defined!" 1>&2
269 return 1
272 OIFS=${IFS}
273 IFS=$'\n'
274 NICKNAMES=( $( certutil -L -d "${FF_HOME}" | sed '1,4d' | grep -F -v ',,' | gawk 'NF--' ) )
275 IFS=${OIFS}
277 for NICKNAME in "${NICKNAMES[@]}"
279 # print the PEM from the cert8.db and get the FP with openssl
280 FP=$( certutil -L -n "${NICKNAME}" -a -d "${FF_HOME}" | openssl x509 -noout -fingerprint -sha1 | sed 's/^.*Fingerprint=//' )
281 if [ -z "${FP}" ]
282 then
283 echo "[-] WARNING: could not get fingerprint for \`${NICKNAME}'!" 1>&2
285 #FPS+=( $( certutil -L -n "${NICKNAME}" -a -d "${FF_HOME}" | openssl x509 -noout -fingerprint -sha1 | sed 's/^.*Fingerprint=//' ) )
286 FPS+=( ${FP} )
287 done
289 echo -e "reverse index:\n"
291 for FP in ${FPS[*]}
293 if [ -n "${CAs[${FP}]}" ]
294 then
295 echo "${CAs[${FP}]}"
296 else
297 echo "[-] WARNING: \`${NICKNAME}' not found (fp=${FP})!" 1>&2
299 done
301 echo -n $'\n'
302 } # reverse_index()
304 function index_cas() {
305 local FILE
306 local -a FILES=()
307 local FP
308 local NICKNAME
309 local OIFS
310 local -a CERT8S=()
311 local CERT8
312 local -a NICKNAMES
314 # index the certs to CAs[] associative array
315 echo "indexing ${CERT_PATH}"
317 # dereference symbolic links
318 for FILE in ${CERT_PATH}/*
320 if [ ! -f "${FILE}" ]
321 then
322 continue
324 FILE=$( expand_cert_path "${FILE}" )
325 FILES+=( "${FILE}" )
326 done
328 for FILE in ${FILES[*]}
330 if [ ! -f "${FILE}" ]
331 then
332 continue
334 FP=$( openssl x509 -in "${FILE}" -noout -fingerprint -sha1 | sed 's/^.*Fingerprint=//' )
335 if [ -n "${FP}" ]
336 then
337 CAs["${FP}"]="${FILE}"
339 done
341 echo -e "${#CAs[*]} CAs\n"
343 if [ -z "${OLD_FF_HOME}" ]
344 then
345 echo "[-] WARNING: OLD_FF_HOME not defined -> returning" 1>&2
346 return 1
349 # cert8.db
350 # use all available firefox profiles
351 if (( INDEX_ALL_CERT8 ))
352 then
353 OIFS=${IFS}
354 IFS=$'\n'
355 # find all cert8.db files under ~/.mozilla/firefox
356 CERT8S=( $( find ~/.mozilla/firefox -type f -name cert8.db | sed 's/\/cert8\.db$//' ) )
357 IFS=${OIFS}
358 else
359 CERT8S=( "${OLD_FF_HOME}" )
361 for CERT8 in "${CERT8S[@]}"
363 echo "indexing cert8.db from \`${CERT8}'"
364 OIFS=${IFS}
365 IFS=$'\n'
366 # get the "nicknames", as this is the way certutil handles the certs
367 NICKNAMES=( $( certutil -L -d "${CERT8}" | sed '1,4d' | gawk 'NF--' ) )
368 IFS=${OIFS}
369 for NICKNAME in "${NICKNAMES[@]}"
371 # print the PEM from the cert8.db and get the FP with openssl
372 FP=$( certutil -L -n "${NICKNAME}" -a -d "${CERT8}" | openssl x509 -noout -fingerprint -sha1 | sed 's/^.*Fingerprint=//' )
373 if [ -n "${FP}" ]
374 then
375 #echo "${NICKNAME}: ${FP}"
376 CERT8_CAs["${FP}"]="${NICKNAME}"
377 #CERT8_NICKS["${NICKNAME}"]="${FP}"
379 done
380 done
381 echo -e "${#CERT8_CAs[*]} CAs\n"
383 return
384 } # index_cas()
386 function rename_libnssckbi() {
387 #find /usr/{lib,lib64} -name 'libnssckbi.so'
388 find /usr/{lib,lib64} -name 'libnssckbi.so' -exec mv -v '{}' '{}.saved' \;
389 # this is because:
390 # lrwxrwxrwx 1 root root 19 Sep 22 20:27 /usr/lib64/seamonkey-2.29/libnssckbi.so -> libnssckbi.so.saved
391 # results into:
392 # mv: '/usr/lib64/seamonkey-2.29/libnssckbi.so' and '/usr/lib64/seamonkey-2.29/libnssckbi.so.saved' are the same file
393 find /usr/{lib,lib64} -name 'libnssckbi.so' -exec rm -v '{}' \;
395 return ${?}
396 } # rename_libnssckbi()
398 function usage() {
399 cat 0<<-EOF
400 ${0##*/} OPTIONS ACTION
402 options:
404 -p path path to CertPatrol's DB (reference)
405 this constructs the list of required CAs
406 -P path path to Firefox profile to update (new profile)
407 -c PEM file path to single root CA PEM file to import
408 -C use "basic list" (${#BASIC_LIST[*]} CAs)
409 -d debug
411 both paths are paths to Firefox's profile directory, e.g. ~/.mozilla/firefox/XXXXXXXX.default
413 actions:
415 -a import required CAs list
416 -A print required CAs list (dry-run)
417 -l get rid of libnssckbi.so
418 -r reverse search
419 (list the certs on the file system that are on the cert8.db)
421 return
422 } # usage()
424 if [ ${#} -eq 0 ]
425 then
426 usage
427 exit
430 while getopts "dhp:P:c:CaAlr" OPTION
432 case "${OPTION}" in
433 "d") DEBUG=1 ;;
434 "h")
435 usage
436 exit 0
438 "p")
439 OLD_FF_HOME="${OPTARG}"
440 CP="${OLD_FF_HOME}/CertPatrol.sqlite"
442 "P")
443 if [ -z "${OPTARG}" ] || [ ! -d "${OPTARG}" ]
444 then
445 echo "[-] error: -P requires an option!" 1>&2
446 exit 1
448 FF_HOME="${OPTARG}"
450 "c")
451 REQUIRED_CAs=( "${OPTARG}" )
453 "C")
454 if [ ! -d /usr/share/ca-certificates ]
455 then
456 echo "[-] error: directory \`/usr/share/ca-certificates' does not exist!" 1>&2
457 echo " you might be running RH/CentOS, which has different system for CAs. see https://github.com/pyllyukko/user.js/issues/140" 1>&2
458 exit 1
460 REQUIRED_CAs=( ${BASIC_LIST[*]/#/\/usr\/share\/ca-certificates\/} )
462 "a") ACTION="import_cas" ;;
463 "A") ACTION="print" ;;
464 "l") ACTION="renamelib" ;;
465 "r") ACTION="rev" ;;
467 usage
469 esac
470 done
472 if [ ${#REQUIRED_CAs[*]} -eq 0 ]
473 then
474 index_cas
475 get_required_cas_list
478 case "${ACTION}" in
479 "import_cas") import_cas ;;
480 "print") print_required_cas_list ;;
481 "rev") reverse_index ;;
482 "renamelib") rename_libnssckbi ;;
484 echo "[-] error: no action defined." 1>&2
485 exit 1
487 esac
489 if [ -n "${FF_HOME}" ]
490 then
491 # list the final result
492 CERT_COUNT=$(( $( certutil -L -d "${FF_HOME}" | wc -l ) - 4 ))
493 echo -e "\ncurrent root CA list from ${FF_HOME}/cert8.db (${CERT_COUNT} certificates):\n"
494 certutil -L -d "${FF_HOME}" | sed 1,4d | grep -v '\(,,\|u,u,u\)'
495 echo -e "\ncountries (check the codes from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements):\n"
496 print_countries