-
Notifications
You must be signed in to change notification settings - Fork 35
/
Copy patheic_parse_authorized_keys
executable file
·355 lines (318 loc) · 15.3 KB
/
eic_parse_authorized_keys
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
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#!/bin/sh
# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
# Reads authorized keys blob $3 and prints verified, unexpired keys
# Openssl to use provided as $1
# Signer public key file path provided as $2
set -e
# Set umask so only we can touch temp files
umask 077
# Failure reporting & exit
# Format: fail [is_debug] [message]
fail () {
if [ "${1}" = true ] ; then
/bin/echo "${2}"
else
/usr/bin/logger -i -p authpriv.info "${2}"
fi
exit 1
}
# Helper to determine if a string starts with a given prefix
# Format: startswith [string] [prefix]
startswith () {
[ "${1#${2}}x" != "${1}x" ]
}
# Helper function to strip out a prefix from a string
# Format: removeprefix [string] [prefix]
removeprefix () {
/usr/bin/printf '%s' "${1#${2}}"
}
# Helper to verify an arbitrary ocsp response body given the certificate and issuer
# Format: verifyocsp [is_debug] [openssl command] [certificate] [issuer] [ocsp directory]
verifyocsp() {
# First check if this cert is already trusted
cname=$("${2}" x509 -noout -subject -in "${3}" 2>/dev/null | /bin/sed -n -e 's/^.*CN[[:blank:]]*=[[:blank:]]*//p')
fingerprint=$("${2}" x509 -noout -fingerprint -sha1 -inform pem -in "${3}" 2>/dev/null | /bin/sed -n 's/SHA1 Fingerprint[[:space:]]*=[[:space:]]*\(.*\)/\1/p' | tr -d ':')
ocsp_out=$("${2}" ocsp -no_nonce -issuer "${4}" -cert "${3}" -VAfile "${4}" -respin "${5}/${fingerprint}" 2>/dev/null)
ocsp_exit="${?}"
if [ "${ocsp_exit}" -ne 0 ] || ! startswith "${ocsp_out}" "${3}: good" ; then
fail "${1}" "EC2 Instance Connect could not verify certificate ${cname} has not been revoked. No keys have been trusted."
fi
}
while getopts ":x:p:o:d:s:i:c:a:v:f:" o; do
case "${o}" in
x)
is_debug="${OPTARG}"
;;
p)
keys_path="${OPTARG}"
;;
o)
OPENSSL="${OPTARG}"
;;
d)
tmpdir="${OPTARG}"
;;
s)
signer="${OPTARG}"
;;
i)
current_instance_id="${OPTARG}"
;;
c)
expected_cn="${OPTARG}"
;;
a)
ca_path="${OPTARG}"
;;
v)
ocsp_dir_path="${OPTARG}"
;;
f)
expected_key="${OPTARG}"
;;
*)
/bin/echo "Usage: $0 [-x debug] [-r key read command] [-o openssl command] [-d tmpdir] [-s signer certificate] [-i instance id] [-c cname] [-a ca path] [-v ocsp dir] [-f key fingerprint]"
exit 1
;;
esac
done
# Verify we have sufficient inputs
if [ $# -lt 1 ] ; then
# No args whatsoever
# Unit test log (ignored by sshd)
/bin/echo "Usage: $0 [-x debug] [-r key read command] [-o openssl command] [-d tmpdir] [-s signer certificate] [-i instance id] [-c cname] [-a ca path] [-v ocsp dir] [-f key fingerprint]"
# System log
/usr/bin/logger -i -p authpriv.info "Unable to run EC2 Instance Connect: insufficient args provided. Please check your sshd configuration."
exit 1
fi
# Verify the signer certificate we have been provided - CN, trust chain, and revocation status
# Split the chain into pieces
/bin/echo "${signer}" | /usr/bin/awk -v dir="${tmpdir}" 'split_after==1{n++;split_after=0} /-----END CERTIFICATE-----/ {split_after=1} {print > dir "/cert" n ".pem"}'
# We want visibility into the CA bundle so we can skip verifying entries in the chain that are already trusted
if [ -d "${ca_path}" ] ; then
ca_path_dir=$ca_path
else
ca_path_dir=$(dirname "${ca_path}")
fi
ca_bundles_dir=$(/bin/mktemp -d "${tmpdir}/eic-cert-XXXXXXXX")
end=$(/usr/bin/find "${tmpdir}" -maxdepth 1 -type f -name "cert*.pem" -regextype sed -regex ".*/cert[0-9]\+\.pem" | wc -l)
if [ "${end}" -gt 0 ] ; then
# First see if we already have them
for i in $(/usr/bin/seq 1 "${end}") ; do
subject=$("${OPENSSL}" x509 -noout -subject -in "${tmpdir}/cert${i}.pem" | /bin/sed -n -e 's/^.*CN[[:space:]]*=[[:space:]]*//p')
underscored=$(/bin/echo "${subject}" | /usr/bin/tr -s ' ' '_') 2>/dev/null
if [ -f "${ca_path_dir}/${underscored}.pem" ] ; then
# We already have it
/bin/cp "${ca_path_dir}/${underscored}.pem" "${ca_bundles_dir}/${underscored}"
else
if [ ! -d "${ca_path}" ] ; then
# Try to pull this CN from the CA bundle
/bin/sed -n -e '/#[[:space:]]'"$subject"'$/,$p' "${ca_path}" 2>/dev/null | /bin/sed '/-----END[[:space:]]CERTIFICATE-----.*/,$d' | /bin/sed -n '1!p' > "${ca_bundles_dir}/${subject}"
if [ -s "${ca_bundles_dir}/${subject}" ] ; then
/bin/echo "-----END CERTIFICATE-----" >> "${ca_bundles_dir}/${subject}"
else
/bin/rm -f "${ca_bundles_dir}/${subject}"
fi
fi
fi
done
fi
# Build the intermediate trust chain
/bin/touch "${tmpdir}/ca-trust.pem"
for i in $(/usr/bin/seq 1 "${end}") ; do
/bin/cat "${tmpdir}/cert${i}.pem" >> "${tmpdir}/ca-trust.pem"
done
if [ -d "${ca_path}" ] ; then
subject=$("${OPENSSL}" x509 -noout -subject -in "${tmpdir}/cert${end}.pem" | /bin/sed -n -e 's/^.*CN[[:space:]]*=[[:space:]]*//p')
underscored=$(/bin/echo "${subject}" | /usr/bin/tr -s ' ' '_') 2>/dev/null
/bin/cat "${ca_bundles_dir}/${underscored}" >> "${tmpdir}/ca-trust.pem" 2>/dev/null
else
/bin/cat "${ca_path}" >> "${tmpdir}/ca-trust.pem" 2>/dev/null
fi
# At this point ca-trust is final
/bin/chmod 400 "${tmpdir}/ca-trust.pem"
# Verify the CN
signer_cn=$("${OPENSSL}" x509 -noout -subject -in "${tmpdir}/cert.pem" | /bin/sed -n -e 's/^.*CN[[:space:]]*=[[:space:]]*//p')
if [ "${signer_cn}" != "${expected_cn}" ] ; then
fail "${is_debug}" "EC2 Instance Connect encountered an unrecognized signer certificate. No keys have been trusted."
fi
# Verify the trust chain
if [ -d "${ca_path}" ] ; then
verify_out=$("${OPENSSL}" verify -x509_strict -CApath "${ca_path}" -CAfile "${tmpdir}/ca-trust.pem" "${tmpdir}/cert.pem")
verify_status=$?
else
# If the CA path is not a directory then do not use it - openssl will throw errors on versions 1.1.1+
verify_out=$("${OPENSSL}" verify -x509_strict -CAfile "${tmpdir}/ca-trust.pem" "${tmpdir}/cert.pem")
verify_status=$?
fi
if [ $verify_status -ne 0 ] || [ "${verify_out}" != "${tmpdir}/cert.pem: OK" ] ; then
fail "${is_debug}" "EC2 Instance Connect could not verify the signer trust chain. No keys have been trusted."
fi
# Verify no certificates have been revoked
# Iterate from first to second-to-last cert & validate OCSP staples
/bin/mv "${tmpdir}/cert.pem" "${tmpdir}/cert0.pem" # Better naming consistency for loop
for i in $(/usr/bin/seq 0 $((end - 1))) ; do
subject=$("${OPENSSL}" x509 -noout -subject -in "${tmpdir}/cert${i}.pem" | /bin/sed -n -e 's/^.*CN[[:space:]]*=[[:space:]]*//p')
if [ -f "${ca_bundles_dir}/${subject}" ] ; then
# If we encounter a certificate that's in the CA bundle we can skip the rest as implicitly trusted
hash=$("${OPENSSL}" x509 -hash -noout -in "${tmpdir}/cert${i}.pem" 2>/dev/null)
trusted_hash=$("${OPENSSL}" x509 -hash -noout -in "${ca_bundles_dir}/${subject}" 2>/dev/null)
fingerprint=$("${OPENSSL}" x509 -noout -fingerprint -sha1 -in "${tmpdir}/cert${i}.pem" 2>/dev/null | /bin/sed -n 's/SHA1 Fingerprint[[:space:]]*=[[:space:]]*\(.*\)/\1/p' | tr -d ':')
trusted_fingerprint=$("${OPENSSL}" x509 -noout -fingerprint -sha1 -in "${ca_bundles_dir}/${subject}" 2>/dev/null | /bin/sed -n 's/SHA1 Fingerprint[[:space:]]*=[[:space:]]*\(.*\)/\1/p' | tr -d ':')
pkey=$("${OPENSSL}" x509 -pubkey -noout -in "${tmpdir}/cert${i}.pem")
trusted_pkey=$("${OPENSSL}" x509 -pubkey -noout -in "${ca_bundles_dir}/${subject}" )
if [ "${hash}" = "${trusted_hash}" ] && [ "${fingerprint}" = "${trusted_fingerprint}" ] && [ "${pkey}" = "${trusted_pkey}" ] ; then
# Already trusted, no need to OCSP verify
break
fi
fi
verifyocsp "${is_debug}" "${OPENSSL}" "${tmpdir}/cert${i}.pem" "${tmpdir}/cert$((i + 1)).pem" "${ocsp_dir_path}"
done
# At this point we no longer need the CA information
/bin/rm -rf "${ca_bundles_dir}"
# Extract cert public key
/bin/echo "${signer}" | "${OPENSSL}" x509 -pubkey -noout > "${tmpdir}/pubkey" 2>/dev/null
extract="${?}"
if [ "${extract}" -ne 0 ] ; then
fail "${is_debug}" "EC2 Instance Connect failed to extract the public key from the signer certificate. No keys have been trusted."
fi
# Begin actual parsing of authorized keys data
if [ -n "${expected_key+x}" ] ; then
# An expected fingerprint was given
if [ "${is_debug}" = false ] ; then
/usr/bin/logger -i -p authpriv.info "Querying EC2 Instance Connect keys for matching fingerprint: ${expected_key}"
fi
fi
# Set current time as expiration marker
curtime=$(/bin/date +%s)
# We want to prevent variables from leaving the parser's scope
# We also want to capture overall exit code
# We also need to redirect the input into our loop
# The simplest solution to all of the above is to take advantage of how sh pipes spawn a subprocess
output=$(
exitcode=255 # Exit code if no valid keys are provided
count=0
# Read loop - pull timestamp line at start of iteration
while read -r line
do
pathprefix="${tmpdir}/${count}"
# Clear our temp buffers to prevent any sort of injection
/bin/rm -f "${pathprefix}-key"
/bin/rm -f "${pathprefix}-signedData"
/bin/rm -f "${pathprefix}-sig"
/bin/rm -f "${pathprefix}-decoded"
# Pre-initialize key validation fields
timestamp=0
instance_id=""
caller=""
request=""
# We do not pre-initialize the actual key or signature fields
/bin/touch "${pathprefix}-signedData"
# Loop to read keys & parse out values
# This is not sub-shelled as we want to maintain variable scope with the outer loop
# Loop condition is until we reach a line that lacks the "#Key" format
while startswith "${line}" "#"
do
# Note that not all of these may be present depending on service deployments
# Similarly, this list is not meant to be exhaustive - there may be new fields to be checked in a later version
if startswith "${line}" "#Timestamp=" ; then
timestamp=$(removeprefix "${line}" "#Timestamp=")
elif startswith "${line}" "#Instance=" ; then
instance_id=$(removeprefix "${line}" "#Instance=")
elif startswith "${line}" "#Caller=" ; then
caller=$(removeprefix "${line}" "#Caller=")
elif startswith "${line}" "#Request=" ; then
request=$(removeprefix "${line}" "#Request=")
# Otherwise it's a #Key we don't recognize (i.e., this version of AuthorizedKeysCommand is outdated)
fi
# We verify on all fields in-order, whether we recognize them or not. Similarly, we don't force fields not present.
# As such we always add this to the signature verification file
/usr/bin/printf '%s\n' "${line}" >> "${pathprefix}-signedData"
# Read the next line
read -r line
done
# At this point, line should contain the key
if startswith "${line}" "ssh" ; then
key="${line}"
/usr/bin/printf '%s\n' "${key}" >> "${pathprefix}-signedData"
# At this point we do not need to modify signedData
/bin/chmod 400 "${pathprefix}-signedData"
# Read key signature - may be multi-line
encodedsigfile="${pathprefix}-sig"
/bin/touch "${encodedsigfile}"
read -r sigline || sigline=""
while [ "${sigline}" != "" ]
do
/usr/bin/printf '%s\n' "${sigline}" >> "${encodedsigfile}"
read -r sigline || sigline=""
done
/bin/chmod 400 "${encodedsigfile}"
/usr/bin/printf '%s\n' "${key}" > "${pathprefix}-key"
# Begin validation
if [ -n "${instance_id}" ] && [ "${timestamp}" -ne 0 ] ; then
fingerprint=$(/usr/bin/ssh-keygen -lf "${pathprefix}-key" | cut -d ' ' -f 2) # Get only the actual fingerprint, ignore key size & source
# If we were told to expect a specific key and this isn't it, skip it
if [ -z "${expected_key}" ] || [ "$fingerprint" = "${expected_key}" ] ; then
# Check instance ID matches & timestamp is still valid
if [ "${current_instance_id}" = "${instance_id}" ] && [ "${timestamp}" -gt "${curtime}" ] ; then
# Decode the signature
(/usr/bin/base64 --decode "${encodedsigfile}" > "${pathprefix}-decoded" || rm -f "${pathprefix}-decoded") 2>/dev/null
if [ -f "${pathprefix}-decoded" ] ; then
# Verify signature
$OPENSSL dgst -sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:32 -verify "${tmpdir}/pubkey" -signature "${pathprefix}-decoded" "${pathprefix}-signedData" 1>/dev/null 2>/dev/null
verify="${?}"
if [ "${verify}" -eq 0 ] ; then
# Signature verified.
# Record this in syslog
callermessage="Providing ssh key from EC2 Instance Connect with fingerprint: ${fingerprint}"
if [ "${request}" != "" ] ; then
callermessage="${callermessage}, request-id: ${request}"
fi
if [ "${caller}" != "" ] ; then
callermessage="${callermessage}, for IAM principal: ${caller}"
fi
if [ "${is_debug}" = false ] ; then
/usr/bin/logger -i -p authpriv.info "${callermessage}"
fi
# Return key to the ssh daemon
/bin/echo "${key}"
exitcode=0
fi
fi
fi
fi
fi
else
# We didn't find a key. Skip until we hit a blank line or EOF
while [ "${line}" != "" ]
do
read -r line || line=""
done
fi
# Clean up any tempfiles
/bin/rm -f "${pathprefix}-key"
/bin/rm -f "${pathprefix}-signedData"
/bin/rm -f "${pathprefix}-sig"
/bin/rm -f "${pathprefix}-decoded"
count=$((count + 1))
done < "${keys_path}"
# This is the loop subprocess's exit code, not the script's
exit $exitcode
)
# Re-capture the exit code
exitcode=$?
# Print keys & exit
/bin/rm -rf "${tmpdir}/pubkey"
/bin/echo "${output}"
exit $exitcode