improve marginal UI for cases when host key can't be retrieved
[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 # output the key info, including the RSA fingerprint
19 show_key_info() {
20     local keyid="$1"
21     local sshKeyGPGFile
22     local sshFingerprint
23     local gpgSigOut
24     local otherUids
25
26     # get the ssh key of the gpg key
27     sshKeyGPGFile=$(msmktempfile)
28     gpg2ssh "$keyid" >"$sshKeyGPGFile"
29     sshFingerprint=$(ssh-keygen -l -f "$sshKeyGPGFile" | \
30         awk '{ print $2 }')
31     rm -f "$sshKeyGPGFile"
32
33     # get the sigs for the matching key
34     gpgSigOut=$(gpg_user --check-sigs \
35         --list-options show-uid-validity \
36         "$keyid")
37
38     echo | log info
39
40     # output the sigs, but only those on the user ID
41     # we are looking for
42     echo "$gpgSigOut" | awk '
43 {
44 if (match($0,"^pub")) { print; }
45 if (match($0,"^uid")) { ok=0; }
46 if (match($0,"^uid.*'$userID'$")) { ok=1; print; }
47 if (ok) { if (match($0,"^sig")) { print; } }
48 }
49 '
50
51     # output ssh fingerprint
52     cat <<EOF
53 RSA key fingerprint is ${sshFingerprint}.
54 EOF
55
56     # output the other user IDs for reference
57     otherUids=$(echo "$gpgSigOut" | grep "^uid" | grep -v "$userID")
58     if [ "$otherUids" ] ; then
59         log info <<EOF
60 Other user IDs on this key:
61 EOF
62         echo "$otherUids" | log info
63     fi
64
65 }
66
67 # "marginal case" ouput in the case that there is not a full
68 # validation path to the host
69 output_no_valid_key() {
70     local userID
71     local sshKeyOffered
72     local gpgOut
73     local type
74     local validity
75     local keyid
76     local uidfpr
77     local usage
78     local sshKeyGPG
79     local tmpkey
80     local returnCode=0
81
82     userID="ssh://${HOSTP}"
83
84     LOG_PREFIX=
85
86     # retrieve the ssh key being offered by the host
87     sshKeyOffered=$(ssh-keyscan -t rsa -p "$PORT" "$HOST" 2>/dev/null \
88         | awk '{ print $2, $3 }')
89
90     # get the gpg info for userid
91     gpgOut=$(gpg_user --list-key --fixed-list-mode --with-colon \
92         --with-fingerprint --with-fingerprint \
93         ="$userID" 2>/dev/null)
94
95     # output header
96     log info <<EOF
97 -------------------- Monkeysphere warning -------------------
98 Monkeysphere found OpenPGP keys for this hostname, but none had full validity.
99 EOF
100
101     # output message if host key could not be retrieved from the host
102     if [ -z "$sshKeyOffered" ] ; then
103         log info <<EOF
104 Could not retrieve RSA host key from $HOST.
105 The following keys were found with marginal validity:
106 EOF
107     fi
108
109     # find all 'pub' and 'sub' lines in the gpg output, which each
110     # represent a retrieved key for the user ID
111     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
112     while IFS=: read -r type validity keyid uidfpr usage ; do
113         case $type in
114             'pub'|'sub')
115                 # get the ssh key of the gpg key
116                 sshKeyGPG=$(gpg2ssh "$keyid")
117
118                 # if a key was retrieved from the host...
119                 if [ "$sshKeyOffered" ] ; then
120
121                     # if one of keys found matches the one offered by the
122                     # host, then output info
123                     if [ "$sshKeyGPG" = "$sshKeyOffered" ] ; then
124                         log info <<EOF
125 An OpenPGP key matching the ssh key offered by the host was found:
126 EOF
127
128                         show_key_info "$keyid" | log info
129
130                         # this whole process is in a "while read"
131                         # subshell.  the only way to get information
132                         # out of the subshell is to change the return
133                         # code.  therefore we return 1 here to
134                         # indicate that a matching gpg key was found
135                         # for the ssh key offered by the host
136                         return 1
137                     fi
138
139                 # else if a key was not retrieved from the host
140                 else
141
142                     # if the current key is marginal, show info
143                     if [ "$validity" = 'm' -o "$validity" = 'f' ] ; then
144                         show_key_info "$keyid" | log info
145                     fi
146
147                 fi
148                 ;;
149         esac
150     done || returnCode="$?"
151
152     # if no key match was made (and the "while read" subshell
153     # returned 1) output how many keys were found
154     if (( returnCode != 1 )) ; then
155
156         echo | log info
157
158         # output different footer messages depending on if a key had
159         # been retrieved from the host
160         if [ "$sshKeyOffered" ] ; then
161             log info <<EOF
162 None of the found keys matched the key offered by the host.
163 EOF
164         else
165             log info <<EOF
166 There may be other keys with less than marginal validity for this hostname.
167 EOF
168         fi
169
170         log info <<EOF
171 Run the following command for more info about the found keys:
172 gpg --check-sigs --list-options show-uid-validity =${userID}
173 EOF
174
175         # FIXME: should we do anything extra here if the retrieved
176         # host key is actually in the known_hosts file and the ssh
177         # connection will succeed?  Should the user be warned?
178         # prompted?
179     fi
180
181     # output footer
182     log info <<EOF
183 -------------------- ssh continues below --------------------
184 EOF
185 }
186
187
188 # the ssh proxycommand function itself
189 ssh_proxycommand() {
190
191 if [ "$1" = '--no-connect' ] ; then
192     NO_CONNECT='true'
193     shift 1
194 fi
195
196 HOST="$1"
197 PORT="$2"
198
199 if [ -z "$HOST" ] ; then
200     log error "Host not specified."
201     usage
202     exit 255
203 fi
204 if [ -z "$PORT" ] ; then
205     PORT=22
206 fi
207
208 # set the host URI
209 if [ "$PORT" != '22' ] ; then
210     HOSTP="${HOST}:${PORT}"
211 else
212     HOSTP="${HOST}"
213 fi
214 URI="ssh://${HOSTP}"
215
216 # specify keyserver checking.  the behavior of this proxy command is
217 # intentionally different than that of running monkeyesphere normally,
218 # and keyserver checking is intentionally done under certain
219 # circumstances.  This can be overridden by setting the
220 # MONKEYSPHERE_CHECK_KEYSERVER environment variable, or by setting the
221 # CHECK_KEYSERVER variable in the monkeysphere.conf file.
222
223 # if the host is in the gpg keyring...
224 if gpg_user --list-key ="${URI}" &>/dev/null ; then
225     # do not check the keyserver
226     CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
227
228 # if the host is NOT in the keyring...
229 else
230     # if the host key is found in the known_hosts file...
231     # FIXME: this only works for default known_hosts location
232     hostKey=$(ssh-keygen -F "$HOST" 2>/dev/null)
233
234     if [ "$hostKey" ] ; then
235         # do not check the keyserver
236         # FIXME: more nuanced checking should be done here to properly
237         # take into consideration hosts that join monkeysphere by
238         # converting an existing and known ssh key
239         CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
240
241     # if the host key is not found in the known_hosts file...
242     else
243         # check the keyserver
244         CHECK_KEYSERVER=${CHECK_KEYSERVER:="true"}
245     fi
246 fi
247
248 # finally look in the MONKEYSPHERE_ environment variable for a
249 # CHECK_KEYSERVER setting to override all else
250 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=$CHECK_KEYSERVER}
251
252 # update the known_hosts file for the host
253 local returnCode=0
254 update_known_hosts "$HOSTP" || returnCode="$?"
255
256 # output on depending on the return of the update-known_hosts
257 # subcommand, which is (ultimately) the return code of the
258 # update_known_hosts function in common
259 case "$returnCode" in
260     0)
261         # acceptable host key found so continue to ssh
262         true
263         ;;
264     1)
265         # no hosts at all found so also continue (drop through to
266         # regular ssh host verification)
267         true
268         ;;
269     2)
270         # at least one *bad* host key (and no good host keys) was
271         # found, so output some usefull information
272         output_no_valid_key
273         ;;
274     *)
275         # anything else drop through
276         true
277         ;;
278 esac
279
280 # FIXME: what about the case where monkeysphere successfully finds a
281 # valid key for the host and adds it to the known_hosts file, but a
282 # different non-monkeysphere key for the host already exists in the
283 # known_hosts, and it is this non-ms key that is offered by the host?
284 # monkeysphere will succeed, and the ssh connection will succeed, and
285 # the user will be left with the impression that they are dealing with
286 # a OpenPGP/PKI host key when in fact they are not.  should we use
287 # ssh-keyscan to compare the keys first?
288
289 # exec a netcat passthrough to host for the ssh connection
290 if [ -z "$NO_CONNECT" ] ; then
291     if (type nc &>/dev/null); then
292         exec nc "$HOST" "$PORT"
293     elif (type socat &>/dev/null); then
294         exec socat STDIO "TCP:$HOST:$PORT"
295     else
296         echo "Neither netcat nor socat found -- could not complete monkeysphere-ssh-proxycommand connection to $HOST:$PORT" >&2
297         exit 255
298     fi
299 fi
300
301 }