Browse code

Add check-tls-x509-fullchains.

Xavier G authored on07/03/2021 17:30:25
Showing1 changed files

1 1
new file mode 100755
... ...
@@ -0,0 +1,194 @@
1
+#!/usr/bin/env bash
2
+
3
+# Check the X.509 fullchain of a remote TLS server. Requires openssl.
4
+#
5
+# (c) 2021 - Xavier G.
6
+# This program is free software. It comes without any warranty, to
7
+# the extent permitted by applicable law. You can redistribute it
8
+# and/or modify it under the terms of the Do What The Fuck You Want
9
+# To Public License, Version 2, as published by Sam Hocevar. See
10
+# http://www.wtfpl.net/ for more details.
11
+
12
+
13
+function make_tmpdir {
14
+	tmp_dir=$(mktemp --directory --suffix .check-fullchain)
15
+	[ -z "${tmp_dir}" ] && _exit 99 "Cannot create temporary directory."
16
+	cd "${tmp_dir}" || _exit 98 "Cannot enter temporary directory ${tmp_dir}."
17
+}
18
+
19
+function cleanup {
20
+	[ -d "${tmp_dir}" ] || return
21
+	rm -f "${tmp_dir}"/*.pem "${tmp_dir}/verify_out"
22
+	rmdir "${tmp_dir}"
23
+}
24
+
25
+function _exit {
26
+	rc="${1}"; shift
27
+	[ "${rc}" -ne 0 ] && echo "$@";
28
+	exit "${rc}"
29
+}
30
+
31
+function extract_pem {
32
+	perl -nle '
33
+		open($fh, q[>], sprintf(q[%d.pem], $i++)) if (m#^-+BEGIN CERTIFICATE-+$#);
34
+		print $fh $_ if $fh;
35
+		close($fh) if m#^-+END CERTIFICATE-+$#;
36
+		# Require at least one cert to exit(0):
37
+		END { exit(!$i); }
38
+	'
39
+}
40
+
41
+function extract_identifier {
42
+	perl -nle 'print $1 if m#((?:[0-9A-F]{2}:)+[0-9A-F]{2})#'
43
+}
44
+
45
+function x509_prop {
46
+	local cert_file="${1}"; shift
47
+	openssl x509 -in "${cert_file}" -noout "$@"
48
+}
49
+
50
+function x509_prop_val {
51
+	x509_prop "$@" | perl -ple 's/^[^=]+=//'
52
+}
53
+
54
+function indent {
55
+	sed 's/^/\t/' "$@"
56
+}
57
+
58
+function pem_show_properties {
59
+	local pem="${1}"
60
+
61
+	echo 'Properties:'
62
+	{
63
+		x509_prop "${pem}" -ext subjectKeyIdentifier -serial
64
+		x509_prop "${pem}" -subject -ext subjectAltName -dates -fingerprint
65
+	} | indent
66
+}
67
+
68
+function pem_check_chain {
69
+	local pem="${1}"
70
+	local prev_pem="${2}"
71
+
72
+	echo ''
73
+	echo 'Chain check:'
74
+	{
75
+		local subject prev_issuer
76
+		subject=$(x509_prop_val "${pem}" -subject)
77
+		prev_issuer=$(x509_prop_val "${prev_pem}" -issuer)
78
+		if [ "${prev_issuer}" == "${subject}" ]; then
79
+			echo "[ok] This certificate's subject matches the previous certificate's issuer."
80
+		else
81
+			echo "[!!] This certificate's subject does NOT match the previous certificate's issuer."
82
+		fi
83
+
84
+		local serial prev_auth_serial
85
+		prev_auth_serial=$(x509_prop "${prev_pem}" -ext authorityKeyIdentifier | grep 'serial:' | extract_identifier)
86
+		if [ -n "${prev_auth_serial}" ]; then
87
+			serial=$(x509_prop_val "${pem}" -serial)
88
+			if [ "${prev_auth_serial//:/}" == "${serial}" ]; then
89
+				echo "[ok] This certificate's serial matches the previous certificate's authority serial."
90
+			else
91
+				echo "[!!] This certificate's serial does NOT match the previous certificate's authority serial."
92
+			fi
93
+		fi
94
+
95
+		local subj_key_id prev_auth_key_id
96
+		subj_key_id=$(x509_prop "${pem}" -ext subjectKeyIdentifier | extract_identifier)
97
+		prev_auth_key_id=$(x509_prop "${prev_pem}" -ext authorityKeyIdentifier | grep 'keyid:' | extract_identifier)
98
+		if [ "${prev_auth_key_id}" == "${subj_key_id}" ]; then
99
+			echo "[ok] This certificate's subject key identifier matches the previous certificate's authority key identifier."
100
+		else
101
+			echo "[!!] This certificate's subject key identifier does NOT match the previous certificate's authority key identifier."
102
+		fi
103
+
104
+		local verify_cmd=(openssl verify -partial_chain -trusted "${pem}" "${prev_pem}")
105
+		if "${verify_cmd[@]}" > verify_out 2>&1; then
106
+			echo "[ok] ${verify_cmd[*]} succeeded"
107
+		else
108
+			echo "[!!] ${verify_cmd[*]} failed with exit code $?"
109
+			indent verify_out
110
+		fi
111
+	} | indent
112
+}
113
+
114
+function pem_show_issuer {
115
+	local pem="${1}"
116
+
117
+	echo ''
118
+	echo 'Issuer:'
119
+	{
120
+		x509_prop "${pem}" -issuer
121
+		x509_prop "${pem}" -ext authorityKeyIdentifier
122
+	} | indent
123
+}
124
+
125
+function analyse_pem {
126
+	local pem="${1}"
127
+	local prev_pem="${2}"
128
+
129
+	pem_show_properties "${pem}"
130
+	[ -n "${prev_pem}" ] && pem_check_chain "${pem}" "${prev_pem}"
131
+	pem_show_issuer "${pem}"
132
+}
133
+
134
+function pem_is_self_signed {
135
+	local pem="${1}"
136
+	local subject issuer
137
+	subject=$(x509_prop_val "${pem}" -subject)
138
+	issuer=$(x509_prop_val "${pem}" -issuer)
139
+	[ "${subject}" == "${issuer}" ]
140
+}
141
+
142
+function get_ca_path {
143
+	ca_path="${CA_PATH:-/etc/ssl/certs}"
144
+	[ -d "${ca_path}" ] || _exit 50 "${ca_path} is not a directory; please set CA_PATH."
145
+}
146
+
147
+function lookup_ca {
148
+	get_ca_path
149
+	local pem
150
+	local issuer="${1}"; shift
151
+	local issuer_hash="${1}"; shift
152
+	for pem in "${ca_path}/${issuer_hash}".*; do
153
+		[ "$(x509_prop_val "${pem}" -subject)" != "${issuer}" ] && continue
154
+		# Found it:
155
+		found_ca_path="${pem}"
156
+		return 0
157
+	done
158
+	return 1
159
+}
160
+
161
+function pem_lookup_ca {
162
+	local pem="${1}"
163
+	lookup_ca "$(x509_prop_val "${pem}" -issuer)" "$(x509_prop_val "${pem}" -issuer_hash)"
164
+}
165
+
166
+# Usual boilerplate:
167
+target="${1}"; shift
168
+[ -z "${target}" ] && _exit 100 "Usage: ${0} host:port [s_client args...]"
169
+[[ "${target}" =~ :[0-9]{1,5}$ ]] || target="${target}:${DEFAULT_PORT:-443}"
170
+trap cleanup EXIT
171
+make_tmpdir
172
+
173
+# Get X.509 certificates (as PEM files) from the provided network target:
174
+openssl s_client -connect "${target}" "$@" -showcerts 2> /dev/null < /dev/null | extract_pem || _exit 90
175
+
176
+# Iterate over those certificates:
177
+while read -r pem; do
178
+	echo "${pem/.pem/}:"
179
+	analyse_pem "${pem}" "${prev_pem}" | indent
180
+	prev_pem="${pem}"
181
+done < <(find . -type f -name '*.pem' -printf '%P\n' | sort -n)
182
+
183
+# Handle the lack of root CA certificate at the end of the delivered fullchain:
184
+if ! pem_is_self_signed "${prev_pem}"; then
185
+	# The last certificate is not self-signed, i.e. it is not the root CA certificate.
186
+	# Look up the root CA certificate within the local CA path:
187
+	if pem_lookup_ca "${prev_pem}"; then
188
+		# Found it -- now analyse it as if it had been provided by the remote server:
189
+		pem="${found_ca_path}"
190
+		echo "$(readlink -f "${pem}"):"
191
+		analyse_pem "${pem}" "${prev_pem}" | indent
192
+	fi
193
+fi
194
+_exit 0