#!/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