ac43f0a0f37b7f6a4dcda161041c8771035eba21
[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 ### COMMON VARIABLES
15
16 # managed directories
17 ETC="/etc/monkeysphere"
18 export ETC
19 CACHE="/var/cache/monkeysphere"
20 export CACHE
21
22 ########################################################################
23 ### UTILITY FUNCTIONS
24
25 failure() {
26     echo "$1" >&2
27     exit ${2:-'1'}
28 }
29
30 # write output to stderr
31 log() {
32     echo -n "ms: " 1>&2
33     echo "$@" 1>&2
34 }
35
36 loge() {
37     echo "$@" 1>&2
38 }
39
40 # cut out all comments(#) and blank lines from standard input
41 meat() {
42     grep -v -e "^[[:space:]]*#" -e '^$'
43 }
44
45 # cut a specified line from standard input
46 cutline() {
47     head --line="$1" | tail -1
48 }
49
50 # check that characters are in a string (in an AND fashion).
51 # used for checking key capability
52 # check_capability capability a [b...]
53 check_capability() {
54     local usage
55     local capcheck
56
57     usage="$1"
58     shift 1
59
60     for capcheck ; do
61         if echo "$usage" | grep -q -v "$capcheck" ; then
62             return 1
63         fi
64     done
65     return 0
66 }
67
68 # convert escaped characters from gpg output back into original
69 # character
70 # FIXME: undo all escape character translation in with-colons gpg output
71 unescape() {
72     echo "$1" | sed 's/\\x3a/:/'
73 }
74
75 # remove all lines with specified string from specified file
76 remove_file_line() {
77     local file
78     local string
79
80     file="$1"
81     string="$2"
82
83     if [ "$file" -a "$string" ] ; then
84         grep -v "$string" "$file" | sponge "$file"
85     fi
86 }
87
88 ### CONVERTION UTILITIES
89
90 # output the ssh key for a given key ID
91 gpg2ssh() {
92     local keyID
93     
94     #keyID="$1" #TMP
95     # only use last 16 characters until openpgp2ssh can take all 40 #TMP
96     keyID=$(echo "$1" | cut -c 25-) #TMP
97
98     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
99 }
100
101 # output known_hosts line from ssh key
102 ssh2known_hosts() {
103     local host
104     local key
105
106     host="$1"
107     key="$2"
108
109     echo -n "$host "
110     echo -n "$key" | tr -d '\n'
111     echo " MonkeySphere${DATE}"
112 }
113
114 # output authorized_keys line from ssh key
115 ssh2authorized_keys() {
116     local userID
117     local key
118     
119     userID="$1"
120     key="$2"
121
122     echo -n "$key" | tr -d '\n'
123     echo " MonkeySphere${DATE} ${userID}"
124 }
125
126 # convert key from gpg to ssh known_hosts format
127 gpg2known_hosts() {
128     local host
129     local keyID
130
131     host="$1"
132     keyID="$2"
133
134     # NOTE: it seems that ssh-keygen -R removes all comment fields from
135     # all lines in the known_hosts file.  why?
136     # NOTE: just in case, the COMMENT can be matched with the
137     # following regexp:
138     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
139     echo -n "$host "
140     gpg2ssh "$keyID" | tr -d '\n'
141     echo " MonkeySphere${DATE}"
142 }
143
144 # convert key from gpg to ssh authorized_keys format
145 gpg2authorized_keys() {
146     local userID
147     local keyID
148
149     userID="$1"
150     keyID="$2"
151
152     # NOTE: just in case, the COMMENT can be matched with the
153     # following regexp:
154     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
155     gpg2ssh "$keyID" | tr -d '\n'
156     echo " MonkeySphere${DATE} ${userID}"
157 }
158
159 ### GPG UTILITIES
160
161 # retrieve all keys with given user id from keyserver
162 # FIXME: need to figure out how to retrieve all matching keys
163 # (not just first N (5 in this case))
164 gpg_fetch_userid() {
165     local userID
166
167     userID="$1"
168
169     log -n " checking keyserver $KEYSERVER... "
170     echo 1,2,3,4,5 | \
171         gpg --quiet --batch --with-colons \
172         --command-fd 0 --keyserver "$KEYSERVER" \
173         --search ="$userID" > /dev/null 2>&1
174     loge "done."
175 }
176
177 # get the full fingerprint of a key ID
178 get_key_fingerprint() {
179     local keyID
180
181     keyID="$1"
182
183     gpg --list-key --with-colons --fixed-list-mode \
184         --with-fingerprint "$keyID" | grep "$keyID" | \
185         grep '^fpr:' | cut -d: -f10
186 }
187
188 ########################################################################
189 ### PROCESSING FUNCTIONS
190
191 # userid and key policy checking
192 # the following checks policy on the returned keys
193 # - checks that full key has appropriate valididy (u|f)
194 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
195 # - checks that requested user ID has appropriate validity
196 # (see /usr/share/doc/gnupg/DETAILS.gz)
197 # output is one line for every found key, in the following format:
198 #
199 # flag fingerprint
200 #
201 # "flag" is an acceptability flag, 0 = ok, 1 = bad
202 # "fingerprint" is the fingerprint of the key
203 #
204 # expects global variable: "MODE"
205 process_user_id() {
206     local userID
207     local requiredCapability
208     local requiredPubCapability
209     local gpgOut
210     local type
211     local validity
212     local keyid
213     local uidfpr
214     local usage
215     local keyOK
216     local uidOK
217     local lastKey
218     local lastKeyOK
219     local fingerprint
220
221     userID="$1"
222
223     # set the required key capability based on the mode
224     if [ "$MODE" = 'known_hosts' ] ; then
225         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
226     elif [ "$MODE" = 'authorized_keys' ] ; then
227         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
228     fi
229     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
230
231     # if CHECK_KEYSERVER variable set, check the keyserver
232     # for the user ID
233     if [ "$CHECK_KEYSERVER" = "true" ] ; then
234         gpg_fetch_userid "$userID"
235     fi
236
237     # output gpg info for (exact) userid and store
238     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
239         --with-fingerprint --with-fingerprint \
240         ="$userID" 2>/dev/null)
241
242     # if the gpg query return code is not 0, return 1
243     if [ "$?" -ne 0 ] ; then
244         log "  - key not found."
245         return 1
246     fi
247
248     # loop over all lines in the gpg output and process.
249     # need to do it this way (as opposed to "while read...") so that
250     # variables set in loop will be visible outside of loop
251     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
252     while IFS=: read -r type validity keyid uidfpr usage ; do
253         # process based on record type
254         case $type in
255             'pub') # primary keys
256                 # new key, wipe the slate
257                 keyOK=
258                 uidOK=
259                 lastKey=pub
260                 lastKeyOK=
261                 fingerprint=
262
263                 log " primary key found: $keyid"
264
265                 # if overall key is not valid, skip
266                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
267                     log "  - unacceptable primary key validity ($validity)."
268                     continue
269                 fi
270                 # if overall key is disabled, skip
271                 if check_capability "$usage" 'D' ; then
272                     log "  - key disabled."
273                     continue
274                 fi
275                 # if overall key capability is not ok, skip
276                 if ! check_capability "$usage" $requiredPubCapability ; then
277                     log "  - unacceptable primary key capability ($usage)."
278                     continue
279                 fi
280
281                 # mark overall key as ok
282                 keyOK=true
283
284                 # mark primary key as ok if capability is ok
285                 if check_capability "$usage" $requiredCapability ; then
286                     lastKeyOK=true
287                 fi
288                 ;;
289             'uid') # user ids
290                 # if an acceptable user ID was already found, skip
291                 if [ "$uidOK" ] ; then
292                     continue
293                 fi
294                 # if the user ID does not match, skip
295                 if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
296                     continue
297                 fi
298                 # if the user ID validity is not ok, skip
299                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
300                     continue
301                 fi
302
303                 # mark user ID acceptable
304                 uidOK=true
305
306                 # output a line for the primary key
307                 # 0 = ok, 1 = bad
308                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
309                     log "  * acceptable key found."
310                     echo 0 "$fingerprint"
311                 else
312                     echo 1 "$fingerprint"
313                 fi
314                 ;;
315             'sub') # sub keys
316                 # unset acceptability of last key
317                 lastKey=sub
318                 lastKeyOK=
319                 fingerprint=
320
321                 # if sub key validity is not ok, skip
322                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
323                     continue
324                 fi
325                 # if sub key capability is not ok, skip
326                 if ! check_capability "$usage" $requiredCapability ; then
327                     continue
328                 fi
329
330                 # mark sub key as ok
331                 lastKeyOK=true
332                 ;;
333             'fpr') # key fingerprint
334                 fingerprint="$uidfpr"
335
336                 # if the last key was the pub key, skip
337                 if [ "$lastKey" = pub ] ; then
338                     continue
339                 fi
340                 
341                 # output a line for the last subkey
342                 # 0 = ok, 1 = bad
343                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
344                     log "  * acceptable key found."
345                     echo 0 "$fingerprint"
346                 else
347                     echo 1 "$fingerprint"
348                 fi
349                 ;;
350         esac
351     done
352 }
353
354 # update the cache for userid, and prompt to add file to
355 # authorized_user_ids file if the userid is found in gpg
356 # and not already in file.
357 update_userid() {
358     local userID
359
360     userID="$1"
361
362     log "processing userid: '$userID'"
363
364     # process the user ID to pull it from keyserver
365     process_user_id "$userID" | grep -q "^0 "
366
367     # check if user ID is in the authorized_user_ids file
368     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
369         read -p "user ID not currently authorized.  authorize? [Y|n]: " OK; OK=${OK:=Y}
370         if [ ${OK/y/Y} = 'Y' ] ; then
371             # add if specified
372             log -n " adding user ID to authorized_user_ids file... "
373             echo "$userID" >> "$AUTHORIZED_USER_IDS"
374             loge "done."
375         else
376             # else do nothing
377             log " authorized_user_ids file untouched."
378         fi
379     fi
380 }
381
382 # remove a userid from the authorized_user_ids file
383 remove_userid() {
384     local userID
385
386     userID="$1"
387
388     log "processing userid: '$userID'"
389
390     # check if user ID is in the authorized_user_ids file
391     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
392         log " user ID not currently authorized."
393         return 1
394     fi
395
396     # remove user ID from file
397     log -n " removing user ID '$userID'... "
398     remove_file_line "$AUTHORIZED_USER_IDS" "^${userID}$"
399     loge "done."
400 }
401
402 # process a host in known_host file
403 process_host_known_hosts() {
404     local host
405     local userID
406     local ok
407     local keyid
408     local tmpfile
409
410     host="$1"
411     userID="ssh://${host}"
412
413     log "processing host: $host"
414
415     process_user_id "ssh://${host}" | \
416     while read -r ok keyid ; do
417         sshKey=$(gpg2ssh "$keyid")
418         # remove the old host key line
419         remove_file_line "$KNOWN_HOSTS" "$sshKey"
420         # if key OK, add new host line
421         if [ "$ok" -eq '0' ] ; then
422             # hash if specified
423             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
424                 # FIXME: this is really hackish cause ssh-keygen won't
425                 # hash from stdin to stdout
426                 tmpfile=$(mktemp)
427                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
428                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
429                 cat "$tmpfile" >> "$KNOWN_HOSTS"
430                 rm -f "$tmpfile" "${tmpfile}.old"
431             else
432                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
433             fi
434         fi
435     done
436 }
437
438 # process a uid in an authorized_keys file
439 process_uid_authorized_keys() {
440     local userID
441     local ok
442     local keyid
443
444     userID="$1"
445
446     log "processing user ID: $userID"
447
448     process_user_id "$userID" | \
449     while read -r ok keyid ; do
450         sshKey=$(gpg2ssh "$keyid")
451         # remove the old host key line
452         remove_file_line "$AUTHORIZED_KEYS" "$sshKey"
453         # if key OK, add new host line
454         if [ "$ok" -eq '0' ] ; then
455             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
456         fi
457     done
458 }
459
460 # process known_hosts file
461 # go through line-by-line, extract each host, and process with the
462 # host processing function
463 process_known_hosts() {
464     local hosts
465     local host
466
467     # take all the hosts from the known_hosts file (first field),
468     # grep out all the hashed hosts (lines starting with '|')...
469     cat "$KNOWN_HOSTS" | meat | \
470         cut -d ' ' -f 1 | grep -v '^|.*$' | \
471     while IFS=, read -r -a hosts ; do
472         # and process each host
473         for host in ${hosts[*]} ; do
474             process_host_known_hosts "$host"
475         done
476     done
477 }
478
479 # process an authorized_user_ids file for authorized_keys
480 process_authorized_user_ids() {
481     local userid
482
483     cat "$AUTHORIZED_USER_IDS" | meat | \
484     while read -r userid ; do
485         process_uid_authorized_keys "$userid"
486     done
487 }
488
489 # EXPERIMENTAL (unused) process userids found in authorized_keys file
490 # go through line-by-line, extract monkeysphere userids from comment
491 # fields, and process each userid
492 # NOT WORKING
493 process_authorized_keys() {
494     local authorizedKeys
495     local userID
496
497     authorizedKeys="$1"
498
499     # take all the monkeysphere userids from the authorized_keys file
500     # comment field (third field) that starts with "MonkeySphere uid:"
501     # FIXME: needs to handle authorized_keys options (field 0)
502     cat "$authorizedKeys" | meat | \
503     while read -r options keytype key comment ; do
504         # if the comment field is empty, assume the third field was
505         # the comment
506         if [ -z "$comment" ] ; then
507             comment="$key"
508         fi
509
510         if echo "$comment" | egrep -v -q '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}' ; then
511             continue
512         fi
513         userID=$(echo "$comment" | awk "{ print $2 }")
514         if [ -z "$userID" ] ; then
515             continue
516         fi
517
518         # process the userid
519         log "processing userid: '$userID'"
520         process_user_id "$userID" > /dev/null
521     done
522 }
523
524 ##################################################
525 ### GPG HELPER FUNCTIONS
526
527 # retrieve key from web of trust, and set owner trust to "full"
528 # if key is found.
529 trust_key() {
530     # get the key from the key server
531     if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
532         log "could not retrieve key '$keyID'"
533         return 1
534     fi
535
536     # get key fingerprint
537     fingerprint=$(get_key_fingerprint "$keyID")
538
539     # attach a "non-exportable" signature to the key
540     # this is required for the key to have any validity at all
541     # the 'y's on stdin indicates "yes, i really want to sign"
542     echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
543
544     # import "full" trust for fingerprint into gpg
545     echo ${fingerprint}:5: | gpg --import-ownertrust
546     if [ $? = 0 ] ; then
547         log "owner trust updated."
548     else
549         failure "there was a problem changing owner trust."
550     fi  
551 }
552
553 # publish server key to keyserver
554 publish_server_key() {
555     read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
556     if [ ${OK/y/Y} != 'Y' ] ; then
557         failure "aborting."
558     fi
559
560     # publish host key
561     # FIXME: need to figure out better way to identify host key
562     # dummy command so as not to publish fakes keys during testing
563     # eventually:
564     #gpg --send-keys --keyserver "$KEYSERVER" $(hostname -f)
565     echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)"
566 }