15f52e0764e70973c2939bea16ce9d706dfb9674
[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     sshFingerprint=$(gpg2ssh "$keyid" | "$SYSSHAREDIR/keytrans" sshfpr)
28
29     # get the sigs for the matching key
30     gpgSigOut=$(gpg_user --check-sigs \
31         --list-options show-uid-validity \
32         "$keyid")
33
34     echo | log info
35
36     # output the sigs, but only those on the user ID
37     # we are looking for
38     echo "$gpgSigOut" | awk '
39 {
40 if (match($0,"^pub")) { print; }
41 if (match($0,"^uid")) { ok=0; }
42 if (match($0,"^uid.*'$userID'$")) { ok=1; print; }
43 if (ok) { if (match($0,"^sig")) { print; } }
44 }
45 '
46
47     # output ssh fingerprint
48     cat <<EOF
49 RSA key fingerprint is ${sshFingerprint}.
50 EOF
51
52     # output the other user IDs for reference
53     otherUids=$(echo "$gpgSigOut" | grep "^uid" | grep -v "$userID")
54     if [ "$otherUids" ] ; then
55         log info <<EOF
56
57 Other user IDs on this key:
58 EOF
59         echo "$otherUids" | log info
60     fi
61
62 }
63
64 # "marginal case" ouput in the case that there is not a full
65 # validation path to the host
66 output_no_valid_key() {
67     local userID
68     local sshKeyOffered
69     local gpgOut
70     local type
71     local validity
72     local keyid
73     local uidfpr
74     local usage
75     local sshKeyGPG
76     local tmpkey
77     local returnCode=0
78
79     userID="ssh://${HOSTP}"
80
81     LOG_PREFIX=
82
83     # if we don't have ssh-keyscan, we just don't scan:
84     if ( type ssh-keyscan &>/dev/null ) ; then
85     # retrieve the ssh key being offered by the host
86         sshKeyOffered=$(ssh-keyscan -t rsa -p "$PORT" "$HOST" 2>/dev/null \
87             | awk '{ print $2, $3 }')
88     fi
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 local hostKey
194
195 if [ "$1" = '--no-connect' ] ; then
196     NO_CONNECT='true'
197     shift 1
198 fi
199
200 HOST="$1"
201 PORT="$2"
202
203 if [ -z "$HOST" ] ; then
204     log error "Host not specified."
205     usage
206     exit 255
207 fi
208 if [ -z "$PORT" ] ; then
209     PORT=22
210 fi
211
212 # set the host URI
213 if [ "$PORT" != '22' ] ; then
214     HOSTP="${HOST}:${PORT}"
215 else
216     HOSTP="${HOST}"
217 fi
218 URI="ssh://${HOSTP}"
219
220 # specify keyserver checking.  the behavior of this proxy command is
221 # intentionally different than that of running monkeyesphere normally,
222 # and keyserver checking is intentionally done under certain
223 # circumstances.  This can be overridden by setting the
224 # MONKEYSPHERE_CHECK_KEYSERVER environment variable, or by setting the
225 # CHECK_KEYSERVER variable in the monkeysphere.conf file.
226
227 # if the host is in the gpg keyring...
228 if gpg_user --list-key ="${URI}" &>/dev/null ; then
229     # do not check the keyserver
230     CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
231
232 # if the host is NOT in the keyring...
233 else
234     # FIXME: what about system-wide known_hosts file (/etc/ssh/known_hosts)?
235
236     if [ -r "$KNOWN_HOSTS" ]; then
237     # look up the host key is found in the known_hosts file...
238         if (type ssh-keygen &>/dev/null) ; then
239             hostKey=$(ssh-keygen -F "$HOST" -f "$KNOWN_HOSTS" 2>/dev/null)
240         else
241             # FIXME: we're not dealing with digested known_hosts if we
242             # don't have ssh-keygen
243
244        # But we could do this without needing ssh-keygen.  hashed
245        # known_hosts looks like: |1|X|Y where 1 means SHA1 (nothing
246        # else is defined in openssh sources), X is the salt (same
247        # length as the digest output), base64-encoded, and Y is the
248        # digested hostname (also base64-encoded).
249
250        # see hostfile.{c,h} in openssh sources.
251
252             hostKey=$(cut -f1 -d\  < .ssh/known_hosts | tr ',' '\n' | grep -Fx -e "$HOST" || :)
253         fi
254     fi
255
256     if [ "$hostKey" ] ; then
257         # do not check the keyserver
258         # FIXME: more nuanced checking should be done here to properly
259         # take into consideration hosts that join monkeysphere by
260         # converting an existing and known ssh key
261         CHECK_KEYSERVER=${CHECK_KEYSERVER:="false"}
262
263     # if the host key is not found in the known_hosts file...
264     else
265         # check the keyserver
266         CHECK_KEYSERVER=${CHECK_KEYSERVER:="true"}
267     fi
268 fi
269
270 # finally look in the MONKEYSPHERE_ environment variable for a
271 # CHECK_KEYSERVER setting to override all else
272 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=$CHECK_KEYSERVER}
273
274 declare -i KEYS_PROCESSED=0
275 declare -i KEYS_VALID=0
276
277 # update the known_hosts file for the host
278 source "${MSHAREDIR}/update_known_hosts"
279 update_known_hosts "$HOSTP"
280
281 if ((KEYS_PROCESSED > 0)) && ((KEYS_VALID == 0)) ; then
282     log debug "output ssh marginal ui..."
283     output_no_valid_key
284 fi
285
286 # FIXME: what about the case where monkeysphere successfully finds a
287 # valid key for the host and adds it to the known_hosts file, but a
288 # different non-monkeysphere key for the host already exists in the
289 # known_hosts, and it is this non-ms key that is offered by the host?
290 # monkeysphere will succeed, and the ssh connection will succeed, and
291 # the user will be left with the impression that they are dealing with
292 # a OpenPGP/PKI host key when in fact they are not.  should we use
293 # ssh-keyscan to compare the keys first?
294
295 # exec a netcat passthrough to host for the ssh connection
296 if [ -z "$NO_CONNECT" ] ; then
297     if (type nc &>/dev/null); then
298         exec nc "$HOST" "$PORT"
299     elif (type socat &>/dev/null); then
300         exec socat STDIO "TCP:$HOST:$PORT"
301     else
302         echo "Neither netcat nor socat found -- could not complete monkeysphere-ssh-proxycommand connection to $HOST:$PORT" >&2
303         exit 255
304     fi
305 fi
306
307 }