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