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