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