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