break out proxy command validation code into it's own function (no functional change)
[monkeysphere.git] / src / share / m / ssh_proxycommand
1 # -*-shell-script-*-
2 # This should be sourced by bash (though we welcome changes to make it POSIX sh compliant)
3
4 # Monkeysphere ssh-proxycommand subcommand
5 #
6 # The monkeysphere scripts are written by:
7 # Jameson Rollins <jrollins@finestructure.net>
8 # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
9 #
10 # They are Copyright 2008-2009, and are all released under the GPL,
11 # version 3 or later.
12
13 # This is meant to be run as an ssh ProxyCommand to initiate a
14 # monkeysphere known_hosts update before an ssh connection to host is
15 # established.  Can be added to ~/.ssh/config as follows:
16 #  ProxyCommand monkeysphere ssh-proxycommand %h %p
17
18 validate_monkeysphere() {
19     local hostKey
20
21     # specify keyserver checking.  the behavior of this proxy command
22     # is intentionally different than that of running monkeyesphere
23     # normally, and keyserver checking is intentionally done under
24     # certain circumstances.  This can be overridden by setting the
25     # MONKEYSPHERE_CHECK_KEYSERVER environment variable, or by setting
26     # the CHECK_KEYSERVER variable in the monkeysphere.conf file.
27
28     # if the host is in the gpg keyring...
29     if gpg_user --list-key ="${URI}" &>/dev/null ; then
30         # do not check the keyserver
31         CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
32
33     # if the host is NOT in the keyring...
34     else
35         # FIXME: what about system-wide known_hosts file (/etc/ssh/known_hosts)?
36
37         if [ -r "$KNOWN_HOSTS" ]; then
38             # look up the host key is found in the known_hosts file...
39             if (type ssh-keygen &>/dev/null) ; then
40                 hostKey=$(ssh-keygen -F "$HOST" -f "$KNOWN_HOSTS" 2>/dev/null)
41             else
42                 # FIXME: we're not dealing with digested known_hosts
43                 # if we don't have ssh-keygen
44
45                 # But we could do this without needing ssh-keygen.
46                 # hashed known_hosts looks like: |1|X|Y where 1 means
47                 # SHA1 (nothing else is defined in openssh sources), X
48                 # is the salt (same length as the digest output),
49                 # base64-encoded, and Y is the digested hostname (also
50                 # base64-encoded).
51
52                 # see hostfile.{c,h} in openssh sources.
53
54                 hostKey=$(cut -f1 -d\  < .ssh/known_hosts | tr ',' '\n' | grep -Fx -e "$HOST" || :)
55             fi
56         fi
57
58         if [ "$hostKey" ] ; then
59         # do not check the keyserver
60         # FIXME: more nuanced checking should be done here to properly
61         # take into consideration hosts that join monkeysphere by
62         # converting an existing and known ssh key
63             CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
64
65         # if the host key is not found in the known_hosts file...
66         else
67             # check the keyserver
68             CHECK_KEYSERVER=${CHECK_KEYSERVER:="true"}
69         fi
70     fi
71
72     # finally look in the MONKEYSPHERE_ environment variable for a
73     # CHECK_KEYSERVER setting to override all else
74     CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=$CHECK_KEYSERVER}
75
76     declare -i KEYS_PROCESSED=0
77     declare -i KEYS_VALID=0
78
79     # update the known_hosts file for the host
80     source "${MSHAREDIR}/update_known_hosts"
81     update_known_hosts "$HOSTP"
82
83     if ((KEYS_PROCESSED > 0)) && ((KEYS_VALID == 0)) ; then
84         log debug "output ssh marginal ui..."
85         output_no_valid_key
86     fi
87
88     # FIXME: what about the case where monkeysphere successfully finds
89     # a valid key for the host and adds it to the known_hosts file,
90     # but a different non-monkeysphere key for the host already exists
91     # in the known_hosts, and it is this non-ms key that is offered by
92     # the host?  monkeysphere will succeed, and the ssh connection
93     # will succeed, and the user will be left with the impression that
94     # they are dealing with a OpenPGP/PKI host key when in fact they
95     # are not.  should we use ssh-keyscan to compare the keys first?
96 }
97
98 # output the key info, including the RSA fingerprint
99 show_key_info() {
100     local keyid="$1"
101     local sshKeyGPGFile
102     local sshFingerprint
103     local gpgSigOut
104     local otherUids
105
106     # get the ssh key of the gpg key
107     sshFingerprint=$(gpg2ssh "$keyid" | "$SYSSHAREDIR/keytrans" sshfpr)
108
109     # get the sigs for the matching key
110     gpgSigOut=$(gpg_user --check-sigs \
111         --list-options show-uid-validity \
112         "$keyid")
113
114     echo | log info
115
116     # output the sigs, but only those on the user ID
117     # we are looking for
118     echo "$gpgSigOut" | awk '
119 {
120 if (match($0,"^pub")) { print; }
121 if (match($0,"^uid")) { ok=0; }
122 if (match($0,"^uid.*'$userID'$")) { ok=1; print; }
123 if (ok) { if (match($0,"^sig")) { print; } }
124 }
125 '
126
127     # output ssh fingerprint
128     cat <<EOF
129 RSA key fingerprint is ${sshFingerprint}.
130 EOF
131
132     # output the other user IDs for reference
133     otherUids=$(echo "$gpgSigOut" | grep "^uid" | grep -v "$userID")
134     if [ "$otherUids" ] ; then
135         log info <<EOF
136
137 Other user IDs on this key:
138 EOF
139         echo "$otherUids" | log info
140     fi
141
142 }
143
144 # "marginal case" ouput in the case that there is not a full
145 # validation path to the host
146 output_no_valid_key() {
147     local userID
148     local sshKeyOffered
149     local gpgOut
150     local type
151     local validity
152     local keyid
153     local uidfpr
154     local usage
155     local sshKeyGPG
156     local tmpkey
157     local returnCode=0
158
159     userID="ssh://${HOSTP}"
160
161     LOG_PREFIX=
162
163     # if we don't have ssh-keyscan, we just don't scan:
164     if ( type ssh-keyscan &>/dev/null ) ; then
165     # retrieve the ssh key being offered by the host
166         sshKeyOffered=$(ssh-keyscan -t rsa -p "$PORT" "$HOST" 2>/dev/null \
167             | awk '{ print $2, $3 }')
168     fi
169
170     # get the gpg info for userid
171     gpgOut=$(gpg_user --list-key --fixed-list-mode --with-colon \
172         --with-fingerprint --with-fingerprint \
173         ="$userID" 2>/dev/null)
174
175     # output header
176     log info <<EOF
177 -------------------- Monkeysphere warning -------------------
178 Monkeysphere found OpenPGP keys for this hostname, but none had full validity.
179 EOF
180
181     # output message if host key could not be retrieved from the host
182     if [ -z "$sshKeyOffered" ] ; then
183         log info <<EOF
184 Could not retrieve RSA host key from $HOST.
185 EOF
186         # check that there are any marginally valid keys
187         if echo "$gpgOut" | egrep -q '^(pub|sub):(m|f|u):' ; then
188             log info <<EOF
189 The following keys were found with marginal validity:
190 EOF
191         fi
192     fi
193
194     # find all keys in the gpg output ('pub' and 'sub' lines) and
195     # output the ones that match the host key or that have marginal
196     # validity
197     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
198     while IFS=: read -r type validity keyid uidfpr usage ; do
199         case $type in
200             'pub'|'sub')
201                 # get the ssh key of the gpg key
202                 sshKeyGPG=$(gpg2ssh "$keyid")
203                 # if a key was retrieved from the host...
204                 if [ "$sshKeyOffered" ] ; then
205                     # if one of the keys matches the one offered by
206                     # the host, then output info and return
207                     if [ "$sshKeyGPG" = "$sshKeyOffered" ] ; then
208                         log info <<EOF
209 An OpenPGP key matching the ssh key offered by the host was found:
210 EOF
211                         show_key_info "$keyid" | log info
212                         # this whole process is in a "while read"
213                         # subshell.  the only way to get information
214                         # out of the subshell is to change the return
215                         # code.  therefore we return 1 here to
216                         # indicate that a matching gpg key was found
217                         # for the ssh key offered by the host
218                         return 1
219                     fi
220                 # else if a key was not retrieved from the host...
221                 else
222                     # and the current key is marginal, show info
223                     if [ "$validity" = 'm' ] \
224                         || [ "$validity" = 'f' ] \
225                         || [ "$validity" = 'u' ] ; then
226                         show_key_info "$keyid" | log info
227                     fi
228                 fi
229                 ;;
230         esac
231     done || returnCode="$?"
232
233     # if no key match was made (and the "while read" subshell
234     # returned 1) output how many keys were found
235     if (( returnCode == 1 )) ; then
236         echo | log info
237     else
238         # if a key was retrieved, but didn't match, note this
239         if [ "$sshKeyOffered" ] ; then
240             log info <<EOF
241 None of the found keys matched the key offered by the host.
242 EOF
243         fi
244
245         # note how many invalid keys were found
246         nInvalidKeys=$(echo "$gpgOut" | egrep '^(pub|sub):[^(m|f|u)]:' | wc -l)
247         if ((nInvalidKeys > 0)) ; then
248             log info <<EOF
249 Keys found with less than marginal validity: $nInvalidKeys
250 EOF
251         fi
252
253         log info <<EOF
254 Run the following command for more info about the found keys:
255 gpg --check-sigs --list-options show-uid-validity =${userID}
256 EOF
257
258         # FIXME: should we do anything extra here if the retrieved
259         # host key is actually in the known_hosts file and the ssh
260         # connection will succeed?  Should the user be warned?
261         # prompted?
262     fi
263
264     # output footer
265     log info <<EOF
266 -------------------- ssh continues below --------------------
267 EOF
268 }
269
270
271 # the ssh proxycommand function itself
272 ssh_proxycommand() {
273
274 if [ "$1" = '--no-connect' ] ; then
275     NO_CONNECT='true'
276     shift 1
277 fi
278
279 HOST="$1"
280 PORT="$2"
281
282 if [ -z "$HOST" ] ; then
283     log error "Host not specified."
284     usage
285     exit 255
286 fi
287 if [ -z "$PORT" ] ; then
288     PORT=22
289 fi
290
291 # set the host URI
292 if [ "$PORT" != '22' ] ; then
293     HOSTP="${HOST}:${PORT}"
294 else
295     HOSTP="${HOST}"
296 fi
297 URI="ssh://${HOSTP}"
298
299 validate_monkeysphere
300
301 # exec a netcat passthrough to host for the ssh connection
302 if [ -z "$NO_CONNECT" ] ; then
303     if (type nc &>/dev/null); then
304         exec nc "$HOST" "$PORT"
305     elif (type socat &>/dev/null); then
306         exec socat STDIO "TCP:$HOST:$PORT"
307     else
308         echo "Neither netcat nor socat found -- could not complete monkeysphere-ssh-proxycommand connection to $HOST:$PORT" >&2
309         exit 255
310     fi
311 fi
312
313 }