Merge commit 'dkg/master'
[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 EOF
106         # check that there are any marginally valid keys
107         if echo "$gpgOut" | egrep -q '^(pub|sub):(m|f|u):' ; then
108             log info <<EOF
109 The following keys were found with marginal validity:
110 EOF
111         fi
112     fi
113
114     # find all keys in the gpg output ('pub' and 'sub' lines) and
115     # output the ones that match the host key or that have marginal
116     # validity
117     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
118     while IFS=: read -r type validity keyid uidfpr usage ; do
119         case $type in
120             'pub'|'sub')
121                 # get the ssh key of the gpg key
122                 sshKeyGPG=$(gpg2ssh "$keyid")
123                 # if a key was retrieved from the host...
124                 if [ "$sshKeyOffered" ] ; then
125                     # if one of the keys matches the one offered by
126                     # the host, then output info and return
127                     if [ "$sshKeyGPG" = "$sshKeyOffered" ] ; then
128                         log info <<EOF
129 An OpenPGP key matching the ssh key offered by the host was found:
130 EOF
131                         show_key_info "$keyid" | log info
132                         # this whole process is in a "while read"
133                         # subshell.  the only way to get information
134                         # out of the subshell is to change the return
135                         # code.  therefore we return 1 here to
136                         # indicate that a matching gpg key was found
137                         # for the ssh key offered by the host
138                         return 1
139                     fi
140                 # else if a key was not retrieved from the host...
141                 else
142                     # and the current key is marginal, show info
143                     if [ "$validity" = 'm' ] \
144                         || [ "$validity" = 'f' ] \
145                         || [ "$validity" = 'u' ] ; then
146                         show_key_info "$keyid" | log info
147                     fi
148                 fi
149                 ;;
150         esac
151     done || returnCode="$?"
152
153     # if no key match was made (and the "while read" subshell
154     # returned 1) output how many keys were found
155     if (( returnCode == 1 )) ; then
156         echo | log info
157     else
158         # if a key was retrieved, but didn't match, note this
159         if [ "$sshKeyOffered" ] ; then
160             log info <<EOF
161 None of the found keys matched the key offered by the host.
162 EOF
163         fi
164
165         # note how many invalid keys were found
166         nInvalidKeys=$(echo "$gpgOut" | egrep '^(pub|sub):[^(m|f|u)]:' | wc -l)
167         if ((nInvalidKeys > 0)) ; then
168             log info <<EOF
169 Keys found with less than marginal validity: $nInvalidKeys
170 EOF
171         fi
172
173         log info <<EOF
174 Run the following command for more info about the found keys:
175 gpg --check-sigs --list-options show-uid-validity =${userID}
176 EOF
177
178         # FIXME: should we do anything extra here if the retrieved
179         # host key is actually in the known_hosts file and the ssh
180         # connection will succeed?  Should the user be warned?
181         # prompted?
182     fi
183
184     # output footer
185     log info <<EOF
186 -------------------- ssh continues below --------------------
187 EOF
188 }
189
190
191 # the ssh proxycommand function itself
192 ssh_proxycommand() {
193
194 if [ "$1" = '--no-connect' ] ; then
195     NO_CONNECT='true'
196     shift 1
197 fi
198
199 HOST="$1"
200 PORT="$2"
201
202 if [ -z "$HOST" ] ; then
203     log error "Host not specified."
204     usage
205     exit 255
206 fi
207 if [ -z "$PORT" ] ; then
208     PORT=22
209 fi
210
211 # set the host URI
212 if [ "$PORT" != '22' ] ; then
213     HOSTP="${HOST}:${PORT}"
214 else
215     HOSTP="${HOST}"
216 fi
217 URI="ssh://${HOSTP}"
218
219 # specify keyserver checking.  the behavior of this proxy command is
220 # intentionally different than that of running monkeyesphere normally,
221 # and keyserver checking is intentionally done under certain
222 # circumstances.  This can be overridden by setting the
223 # MONKEYSPHERE_CHECK_KEYSERVER environment variable, or by setting the
224 # CHECK_KEYSERVER variable in the monkeysphere.conf file.
225
226 # if the host is in the gpg keyring...
227 if gpg_user --list-key ="${URI}" &>/dev/null ; then
228     # do not check the keyserver
229     CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
230
231 # if the host is NOT in the keyring...
232 else
233     # if the host key is found in the known_hosts file...
234     hostKey=$( [ ! -r "$KNOWN_HOSTS" ] || ssh-keygen -F "$HOST" -f "$KNOWN_HOSTS" 2>/dev/null)
235
236     if [ "$hostKey" ] ; then
237         # do not check the keyserver
238         # FIXME: more nuanced checking should be done here to properly
239         # take into consideration hosts that join monkeysphere by
240         # converting an existing and known ssh key
241         CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
242
243     # if the host key is not found in the known_hosts file...
244     else
245         # check the keyserver
246         CHECK_KEYSERVER=${CHECK_KEYSERVER:="true"}
247     fi
248 fi
249
250 # finally look in the MONKEYSPHERE_ environment variable for a
251 # CHECK_KEYSERVER setting to override all else
252 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=$CHECK_KEYSERVER}
253
254 # update the known_hosts file for the host
255 local returnCode=0
256 update_known_hosts "$HOSTP" || returnCode="$?"
257
258 # output on depending on the return of the update-known_hosts
259 # subcommand, which is (ultimately) the return code of the
260 # update_known_hosts function in common
261 case "$returnCode" in
262     0)
263         # acceptable host key found so continue to ssh
264         true
265         ;;
266     1)
267         # no hosts at all found so also continue (drop through to
268         # regular ssh host verification)
269         true
270         ;;
271     2)
272         # at least one *bad* host key (and no good host keys) was
273         # found, so output some usefull information
274         output_no_valid_key
275         ;;
276     *)
277         # anything else drop through
278         true
279         ;;
280 esac
281
282 # FIXME: what about the case where monkeysphere successfully finds a
283 # valid key for the host and adds it to the known_hosts file, but a
284 # different non-monkeysphere key for the host already exists in the
285 # known_hosts, and it is this non-ms key that is offered by the host?
286 # monkeysphere will succeed, and the ssh connection will succeed, and
287 # the user will be left with the impression that they are dealing with
288 # a OpenPGP/PKI host key when in fact they are not.  should we use
289 # ssh-keyscan to compare the keys first?
290
291 # exec a netcat passthrough to host for the ssh connection
292 if [ -z "$NO_CONNECT" ] ; then
293     if (type nc &>/dev/null); then
294         exec nc "$HOST" "$PORT"
295     elif (type socat &>/dev/null); then
296         exec socat STDIO "TCP:$HOST:$PORT"
297     else
298         echo "Neither netcat nor socat found -- could not complete monkeysphere-ssh-proxycommand connection to $HOST:$PORT" >&2
299         exit 255
300     fi
301 fi
302
303 }