Current File : //usr/share/ec2-instance-connect/eic_parse_authorized_keys |
#!/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/pI' | 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
# We only need to verify the first intermediate certificate since it's signed by Amazon Root CA 1, which
# is already trusted by the system (in /etc/ssl/certs).
/usr/bin/cp "${tmpdir}/cert1.pem" "${tmpdir}/ca-trust.pem"
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