fix bugs in ssh key export functions
[monkeysphere.git] / src / common
1 # -*-shell-script-*-
2
3 # Shared bash functions for the monkeysphere
4 #
5 # Written by
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 #
8 # Copyright 2008, released under the GPL, version 3 or later
9
10 # all caps variables are meant to be user supplied (ie. from config
11 # file) and are considered global
12
13 ########################################################################
14 # managed directories
15 ETC="/etc/monkeysphere"
16 export ETC
17 CACHE="/var/cache/monkeysphere"
18 export CACHE
19 ########################################################################
20
21 failure() {
22     echo "$1" >&2
23     exit ${2:-'1'}
24 }
25
26 # write output to stdout
27 log() {
28     echo -n "ms: "
29     echo "$@"
30 }
31
32 # write output to stderr
33 loge() {
34     echo -n "ms: " 1>&2
35     echo "$@" 1>&2
36 }
37
38 # cut out all comments(#) and blank lines from standard input
39 meat() {
40     grep -v -e "^[[:space:]]*#" -e '^$'
41 }
42
43 # cut a specified line from standard input
44 cutline() {
45     head --line="$1" | tail -1
46 }
47
48 # retrieve all keys with given user id from keyserver
49 # FIXME: need to figure out how to retrieve all matching keys
50 # (not just first 5)
51 gpg_fetch_keys() {
52     local id
53     id="$1"
54     echo 1,2,3,4,5 | \
55         gpg --quiet --batch --command-fd 0 --with-colons \
56         --keyserver "$KEYSERVER" \
57         --search ="$id" >/dev/null 2>&1
58 }
59
60 # check that characters are in a string (in an AND fashion).
61 # used for checking key capability
62 # check_capability capability a [b...]
63 check_capability() {
64     local capability
65     local capcheck
66
67     capability="$1"
68     shift 1
69
70     for capcheck ; do
71         if echo "$capability" | grep -q -v "$capcheck" ; then
72             return 1
73         fi
74     done
75     return 0
76 }
77
78 # convert escaped characters from gpg output back into original
79 # character
80 # FIXME: undo all escape character translation in with-colons gpg output
81 unescape() {
82     echo "$1" | sed 's/\\x3a/:/'
83 }
84
85 # convert key from gpg to ssh known_hosts format
86 gpg2known_hosts() {
87     local keyID
88     local host
89
90     keyID="$1"
91     host=$(echo "$2" | sed -e "s|ssh://||")
92
93     # NOTE: it seems that ssh-keygen -R removes all comment fields from
94     # all lines in the known_hosts file.  why?
95     # NOTE: just in case, the COMMENT can be matched with the
96     # following regexp:
97     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
98     echo -n "$host "
99     gpg --export "$keyID" | \
100         openpgp2ssh "$keyID" | tr -d '\n'
101     echo " MonkeySphere${DATE}"
102 }
103
104 # convert key from gpg to ssh authorized_keys format
105 gpg2authorized_keys() {
106     local keyID
107     local userID
108
109     keyID="$1"
110     userID="$2"
111
112     gpg --export "$keyID" | \
113         openpgp2ssh "$keyID" | tr -d '\n'
114     echo " MonkeySphere${DATE}:${userID}"
115 }
116
117 # userid and key policy checking
118 # the following checks policy on the returned keys
119 # - checks that full key has appropriate valididy (u|f)
120 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
121 # - checks that particular desired user id has appropriate validity
122 # see /usr/share/doc/gnupg/DETAILS.gz
123 # expects global variable: "MODE"
124 process_user_id() {
125     local userID
126     local cacheDir
127     local requiredPubCapability
128     local gpgOut
129     local line
130     local type
131     local validity
132     local keyid
133     local uidfpr
134     local capability
135     local keyOK
136     local pubKeyID
137     local uidOK
138     local keyIDs
139     local userIDHash
140     local keyID
141
142     userID="$1"
143     cacheDir="$2"
144
145     requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]")
146
147     # fetch keys from keyserver, return 1 if none found
148     gpg_fetch_keys "$userID" || return 1
149
150     # output gpg info for (exact) userid and store
151     gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
152         ="$userID" 2> /dev/null)
153
154     # return 1 if there only "tru" lines are output from gpg
155     if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
156         loge "  key not found."
157         return 1
158     fi
159
160     # loop over all lines in the gpg output and process.
161     # need to do it this way (as opposed to "while read...") so that
162     # variables set in loop will be visible outside of loop
163     for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do
164
165         # read the contents of the line
166         type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1)
167         validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2)
168         keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5)
169         uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10)
170         capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12)
171
172         # process based on record type
173         case $type in
174             'pub') # primary keys
175                 # new key, wipe the slate
176                 keyOK=
177                 pubKeyID=
178                 uidOK=
179                 keyIDs=
180
181                 pubKeyID="$keyid"
182
183                 # check primary key validity
184                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
185                     loge "  unacceptable primary key validity ($validity)."
186                     continue
187                 fi
188                 # check capability is not Disabled...
189                 if check_capability "$capability" 'D' ; then
190                     loge "  key disabled."
191                     continue
192                 fi
193                 # check overall key capability
194                 # must be Encryption and Authentication
195                 if ! check_capability "$capability" $requiredPubCapability ; then
196                     loge "  unacceptable primary key capability ($capability)."
197                     continue
198                 fi
199
200                 # mark if primary key is acceptable
201                 keyOK=true
202
203                 # add primary key ID to key list if it has required capability
204                 if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
205                     keyIDs[${#keyIDs[*]}]="$keyid"
206                 fi
207                 ;;
208             'uid') # user ids
209                 # check key ok and we have key fingerprint
210                 if [ -z "$keyOK" ] ; then
211                     continue
212                 fi
213                 # check key validity
214                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
215                     continue
216                 fi
217                 # check the uid matches
218                 if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
219                     continue
220                 fi
221
222                 # mark if uid acceptable
223                 uidOK=true
224                 ;;
225             'sub') # sub keys
226                 # add sub key ID to key list if it has required capability
227                 if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
228                     keyIDs[${#keyIDs[*]}]="$keyid"
229                 fi
230                 ;;
231         esac
232     done
233
234     # hash userid for cache file name
235     userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }')
236
237     # touch/clear key cache file
238     # (will be left empty if there are noacceptable keys)
239     > "$cacheDir"/"$userIDHash"."$pubKeyID"
240
241     # for each acceptable key, write an ssh key line to the
242     # key cache file
243     if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
244         for keyID in ${keyIDs[@]} ; do
245             loge "  acceptable key/uid found."
246
247             if [ "$MODE" = 'known_hosts' ] ; then
248                 # export the key
249                 gpg2known_hosts "$keyID" "$userID" >> \
250                     "$cacheDir"/"$userIDHash"."$pubKeyID"
251                 # hash the cache file if specified
252                 if [ "$HASH_KNOWN_HOSTS" ] ; then
253                     ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
254                     rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
255                 fi
256             elif [ "$MODE" = 'authorized_keys' ] ; then
257                 # export the key
258                 # FIXME: needs to apply extra options for authorized_keys
259                 # lines if specified
260                 gpg2authorized_keys "$keyID" "$userID" >> \
261                     "$cacheDir"/"$userIDHash"."$pubKeyID"
262             fi
263         done
264     fi
265
266     # echo the path to the key cache file
267     echo "$cacheDir"/"$userIDHash"."$pubKeyID"
268 }
269
270 # process a host for addition to a known_host file
271 process_host() {
272     local host
273     local cacheDir
274     local hostKeyCachePath
275
276     host="$1"
277     cacheDir="$2"
278
279     log "processing host: '$host'"
280
281     hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
282     if [ $? = 0 ] ; then
283         ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
284         cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS"
285     fi
286 }
287
288 # process known_hosts file
289 # go through line-by-line, extract each host, and process with the
290 # host processing function
291 process_known_hosts() {
292     local knownHosts
293     local cacheDir
294     local hosts
295     local host
296
297     knownHosts="$1"
298     cacheDir="$2"
299
300     # take all the hosts from the known_hosts file (first field),
301     # grep out all the hashed hosts (lines starting with '|')
302     cut -d ' ' -f 1 "$knownHosts" | \
303     grep -v '^|.*$' | \
304     while IFS=, read -r -a hosts ; do
305         # process each host
306         for host in ${hosts[*]} ; do
307             process_host "$host" "$cacheDir"
308         done
309     done
310 }
311
312 # update an authorized_keys file after first processing the 
313 # authorized_user_ids file
314 update_authorized_keys() {
315     local msAuthorizedKeys
316     local userAuthorizedKeys
317     local cacheDir
318
319     msAuthorizedKeys="$1"
320     userAuthorizedKeys="$2"
321     cacheDir="$3"
322
323     process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
324
325     # write output key file
326     log "writing monkeysphere authorized_keys file... "
327     touch "$msAuthorizedKeys"
328     if [ "$(ls "$cacheDir")" ] ; then
329         log -n "adding gpg keys... "
330         cat "$cacheDir"/* > "$msAuthorizedKeys"
331         echo "done."
332     else
333         log "no gpg keys to add."
334     fi
335     if [ "$userAuthorizedKeys" -a -s "$userAuthorizedKeys" ] ; then
336         log -n "adding user authorized_keys file... "
337         cat "$userAuthorizedKeys" >> "$msAuthorizedKeys"
338         echo "done."
339     fi
340     log "monkeysphere authorized_keys file generated: $msAuthorizedKeys"
341 }
342
343 # process an authorized_*_ids file
344 # go through line-by-line, extract each userid, and process
345 process_authorized_ids() {
346     local authorizedIDs
347     local cacheDir
348     local userID
349
350     authorizedIDs="$1"
351     cacheDir="$2"
352
353     # clean out keys file and remake keys directory
354     rm -rf "$cacheDir"
355     mkdir -p "$cacheDir"
356
357     # loop through all user ids in file
358     # FIXME: needs to handle authorized_keys options
359     cat "$authorizedIDs" | meat | \
360     while read -r userID ; do
361         # process the userid
362         log "processing userid: '$userID'"
363         process_user_id "$userID" "$cacheDir" > /dev/null
364     done
365 }
366
367 # EXPERIMENTAL (unused) process userids found in authorized_keys file
368 # go through line-by-line, extract monkeysphere userids from comment
369 # fields, and process each userid
370 process_userids_from_authorized_keys() {
371     local authorizedKeys
372     local cacheDir
373     local userID
374
375     authorizedKeys="$1"
376     cacheDir="$2"
377
378     # take all the monkeysphere userids from the authorized_keys file
379     # comment field (third field) that starts with "MonkeySphere uid:"
380     # FIXME: needs to handle authorized_keys options (field 0)
381     cat "$authorizedKeys" | \
382     while read -r options keytype key comment ; do
383         # if the comment field is empty, assume the third field was
384         # the comment
385         if [ -z "$comment" ] ; then
386             comment="$key"
387         fi
388         if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
389             continue
390         fi
391         userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
392         if [ -z "$userID" ] ; then
393             continue
394         fi
395         # process the userid
396         log "processing userid: '$userID'"
397         process_user_id "$userID" "$cacheDir" > /dev/null
398     done
399 }
400
401 # update the cache for userid, and prompt to add file to
402 # authorized_user_ids file if the userid is found in gpg
403 # and not already in file.
404 update_userid() {
405     local userID
406     local cacheDir
407     local userIDKeyCache
408
409     userID="$1"
410     cacheDir="$2"
411
412     log "processing userid: '$userID'"
413     userIDKeyCache=$(process_user_id "$userID" "$cacheDir")
414     if [ -z "$userIDKeyCache" ] ; then
415         return 1
416     fi
417     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
418         echo "the following userid is not in the authorized_user_ids file:"
419         echo "  $userID"
420         read -p "would you like to add? [Y|n]: " OK; OK=${OK:=Y}
421         if [ ${OK/y/Y} = 'Y' ] ; then
422             log -n "  adding userid to authorized_user_ids file... "
423             echo "$userID" >> "$AUTHORIZED_USER_IDS"
424             echo "done."
425         fi
426     fi
427 }
428
429 # retrieve key from web of trust, and set owner trust to "full"
430 # if key is found.
431 trust_key() {
432     # get the key from the key server
433     gpg --keyserver "$KEYSERVER" --recv-key "$keyID" || failure "could not retrieve key '$keyID'"
434
435     # edit the key to change trust
436     # FIXME: need to figure out how to automate this,
437     # in a batch mode or something.
438     gpg --edit-key "$keyID"
439 }