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