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