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