Add 'remove_userid' function, inverse of 'update_userids'.
[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     # make sure the cache directory exists
244     mkdir -p "$cacheDir"
245
246     # touch/clear key cache file
247     # (will be left empty if there are noacceptable keys)
248     > "$cacheDir"/"$userIDHash"."$pubKeyID"
249
250     # for each acceptable key, write an ssh key line to the
251     # key cache file
252     if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
253         for keyID in ${keyIDs[@]} ; do
254             log "  acceptable key/uid found."
255
256             if [ "$MODE" = 'known_hosts' ] ; then
257                 # export the key
258                 gpg2known_hosts "$keyID" "$userID" >> \
259                     "$cacheDir"/"$userIDHash"."$pubKeyID"
260                 # hash the cache file if specified
261                 if [ "$HASH_KNOWN_HOSTS" ] ; then
262                     ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
263                     rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
264                 fi
265             elif [ "$MODE" = 'authorized_keys' ] ; then
266                 # export the key
267                 # FIXME: needs to apply extra options for authorized_keys
268                 # lines if specified
269                 gpg2authorized_keys "$keyID" "$userID" >> \
270                     "$cacheDir"/"$userIDHash"."$pubKeyID"
271             fi
272         done
273     fi
274
275     # echo the path to the key cache file
276     echo "$cacheDir"/"$userIDHash"."$pubKeyID"
277 }
278
279 # update the cache for userid, and prompt to add file to
280 # authorized_user_ids file if the userid is found in gpg
281 # and not already in file.
282 update_userid() {
283     local userID
284     local cacheDir
285     local userIDKeyCache
286
287     userID="$1"
288     cacheDir="$2"
289
290     log "processing userid: '$userID'"
291
292     userIDKeyCache=$(process_user_id "$userID" "$cacheDir")
293
294     if [ -z "$userIDKeyCache" ] ; then
295         return 1
296     fi
297     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
298         read -p "user ID not currently authorized.  authorize? [Y|n]: " OK; OK=${OK:=Y}
299         if [ ${OK/y/Y} = 'Y' ] ; then
300             log -n "adding user ID to authorized_user_ids file... "
301             echo "$userID" >> "$AUTHORIZED_USER_IDS"
302             echo "done."
303         else
304             log "authorized_user_ids file untouched."
305         fi
306     fi
307 }
308
309 # remove a userid from the authorized_user_ids file
310 remove_userid() {
311     local userID
312
313     userID="$1"
314
315     log "processing userid: '$userID'"
316
317     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
318         log "user ID not currently authorized."
319         return 1
320     fi
321
322     log -n "removing user ID '$userID'... "
323     grep -v "$userID" "$AUTHORIZED_USER_IDS" | sponge "$AUTHORIZED_USER_IDS"
324     echo "done."
325 }
326
327 # process a host for addition to a known_host file
328 process_host() {
329     local host
330     local cacheDir
331     local hostKeyCachePath
332
333     host="$1"
334     cacheDir="$2"
335
336     log "processing host: '$host'"
337
338     hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
339     if [ $? = 0 ] ; then
340         ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
341         cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS"
342     fi
343 }
344
345 # process known_hosts file
346 # go through line-by-line, extract each host, and process with the
347 # host processing function
348 process_known_hosts() {
349     local knownHosts
350     local cacheDir
351     local hosts
352     local host
353
354     knownHosts="$1"
355     cacheDir="$2"
356
357     # take all the hosts from the known_hosts file (first field),
358     # grep out all the hashed hosts (lines starting with '|')...
359     cut -d ' ' -f 1 "$knownHosts" | \
360     grep -v '^|.*$' | \
361     while IFS=, read -r -a hosts ; do
362         # ...and process each host
363         for host in ${hosts[*]} ; do
364             process_host "$host" "$cacheDir"
365         done
366     done
367 }
368
369 # update an authorized_keys file after first processing the 
370 # authorized_user_ids file
371 update_authorized_keys() {
372     local msAuthorizedKeys
373     local userAuthorizedKeys
374     local cacheDir
375
376     msAuthorizedKeys="$1"
377     userAuthorizedKeys="$2"
378     cacheDir="$3"
379
380     process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
381
382     # write output key file
383     log "writing monkeysphere authorized_keys file... "
384     touch "$msAuthorizedKeys"
385     if [ "$(ls "$cacheDir")" ] ; then
386         log -n "adding gpg keys... "
387         cat "$cacheDir"/* > "$msAuthorizedKeys"
388         echo "done."
389     else
390         log "no gpg keys to add."
391     fi
392     if [ "$userAuthorizedKeys" -a -s "$userAuthorizedKeys" ] ; then
393         log -n "adding user authorized_keys file... "
394         cat "$userAuthorizedKeys" >> "$msAuthorizedKeys"
395         echo "done."
396     fi
397     log "monkeysphere authorized_keys file generated:"
398     log "$msAuthorizedKeys"
399 }
400
401 # process an authorized_*_ids file
402 # go through line-by-line, extract each userid, and process
403 process_authorized_ids() {
404     local authorizedIDs
405     local cacheDir
406     local userID
407
408     authorizedIDs="$1"
409     cacheDir="$2"
410
411     # clean out keys file and remake keys directory
412     rm -rf "$cacheDir"
413     mkdir -p "$cacheDir"
414
415     # loop through all user ids in file
416     # FIXME: needs to handle authorized_keys options
417     cat "$authorizedIDs" | meat | \
418     while read -r userID ; do
419         # process the userid
420         log "processing userid: '$userID'"
421         process_user_id "$userID" "$cacheDir" > /dev/null
422     done
423 }
424
425 # EXPERIMENTAL (unused) process userids found in authorized_keys file
426 # go through line-by-line, extract monkeysphere userids from comment
427 # fields, and process each userid
428 process_userids_from_authorized_keys() {
429     local authorizedKeys
430     local cacheDir
431     local userID
432
433     authorizedKeys="$1"
434     cacheDir="$2"
435
436     # take all the monkeysphere userids from the authorized_keys file
437     # comment field (third field) that starts with "MonkeySphere uid:"
438     # FIXME: needs to handle authorized_keys options (field 0)
439     cat "$authorizedKeys" | \
440     while read -r options keytype key comment ; do
441         # if the comment field is empty, assume the third field was
442         # the comment
443         if [ -z "$comment" ] ; then
444             comment="$key"
445         fi
446         if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
447             continue
448         fi
449         userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
450         if [ -z "$userID" ] ; then
451             continue
452         fi
453         # process the userid
454         log "processing userid: '$userID'"
455         process_user_id "$userID" "$cacheDir" > /dev/null
456     done
457 }
458
459 # retrieve key from web of trust, and set owner trust to "full"
460 # if key is found.
461 trust_key() {
462     # get the key from the key server
463     if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
464         log "could not retrieve key '$keyID'"
465         return 1
466     fi
467
468     # get key fingerprint
469     fingerprint=$(get_key_fingerprint "$keyID")
470
471     # import "full" trust for fingerprint into gpg
472     echo ${fingerprint}:5: | gpg --import-ownertrust
473     if [ $? = 0 ] ; then
474         log "owner trust updated."
475     else
476         failure "there was a problem changing owner trust."
477     fi  
478 }
479
480 # publish server key to keyserver
481 publish_server_key() {
482     read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
483     if [ ${OK/y/Y} != 'Y' ] ; then
484         failure "aborting."
485     fi
486
487     # publish host key
488     # FIXME: need to figure out better way to identify host key
489     # dummy command so as not to publish fakes keys during testing
490     # eventually:
491     #gpg --send-keys --keyserver "$KEYSERVER" $(hostname -f)
492     echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)"
493 }