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 a host in known_host file
380 process_host_known_hosts() {
381     local host
382     local userID
383     local ok
384     local keyid
385     local tmpfile
386
387     host="$1"
388     userID="ssh://${host}"
389
390     log "processing host: $host"
391
392     process_user_id "ssh://${host}" | \
393     while read -r ok keyid ; do
394         sshKey=$(gpg2ssh "$keyid")
395         # remove the old host key line
396         remove_line "$KNOWN_HOSTS" "$sshKey"
397         # if key OK, add new host line
398         if [ "$ok" -eq '0' ] ; then
399             # hash if specified
400             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
401                 # FIXME: this is really hackish cause ssh-keygen won't
402                 # hash from stdin to stdout
403                 tmpfile=$(mktemp)
404                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
405                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
406                 cat "$tmpfile" >> "$KNOWN_HOSTS"
407                 rm -f "$tmpfile" "${tmpfile}.old"
408             else
409                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
410             fi
411         fi
412     done
413 }
414
415 # process a uid in an authorized_keys file
416 process_uid_authorized_keys() {
417     local userID
418     local ok
419     local keyid
420
421     userID="$1"
422
423     log "processing user ID: $userID"
424
425     process_user_id "$userID" | \
426     while read -r ok keyid ; do
427         sshKey=$(gpg2ssh "$keyid")
428         # remove the old host key line
429         remove_line "$AUTHORIZED_KEYS" "$sshKey"
430         # if key OK, add new host line
431         if [ "$ok" -eq '0' ] ; then
432             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
433         fi
434     done
435 }
436
437 # process known_hosts file
438 # go through line-by-line, extract each host, and process with the
439 # host processing function
440 process_known_hosts() {
441     local hosts
442     local host
443
444     # take all the hosts from the known_hosts file (first field),
445     # grep out all the hashed hosts (lines starting with '|')...
446     cat "$KNOWN_HOSTS" | meat | \
447         cut -d ' ' -f 1 | grep -v '^|.*$' | \
448     while IFS=, read -r -a hosts ; do
449         # and process each host
450         for host in ${hosts[*]} ; do
451             process_host_known_hosts "$host"
452         done
453     done
454 }
455
456 # process an authorized_user_ids file for authorized_keys
457 process_authorized_user_ids() {
458     local userid
459
460     authorizedUserIDs="$1"
461
462     cat "$authorizedUserIDs" | meat | \
463     while read -r userid ; do
464         process_uid_authorized_keys "$userid"
465     done
466 }
467
468 # EXPERIMENTAL (unused) process userids found in authorized_keys file
469 # go through line-by-line, extract monkeysphere userids from comment
470 # fields, and process each userid
471 # NOT WORKING
472 process_authorized_keys() {
473     local authorizedKeys
474     local userID
475
476     authorizedKeys="$1"
477
478     # take all the monkeysphere userids from the authorized_keys file
479     # comment field (third field) that starts with "MonkeySphere uid:"
480     # FIXME: needs to handle authorized_keys options (field 0)
481     cat "$authorizedKeys" | meat | \
482     while read -r options keytype key comment ; do
483         # if the comment field is empty, assume the third field was
484         # the comment
485         if [ -z "$comment" ] ; then
486             comment="$key"
487         fi
488
489         if echo "$comment" | egrep -v -q '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}' ; then
490             continue
491         fi
492         userID=$(echo "$comment" | awk "{ print $2 }")
493         if [ -z "$userID" ] ; then
494             continue
495         fi
496
497         # process the userid
498         log "processing userid: '$userID'"
499         process_user_id "$userID" > /dev/null
500     done
501 }
502
503 ##################################################
504 ### GPG HELPER FUNCTIONS
505
506 # retrieve key from web of trust, and set owner trust to "full"
507 # if key is found.
508 trust_key() {
509     # get the key from the key server
510     if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
511         log "could not retrieve key '$keyID'"
512         return 1
513     fi
514
515     # get key fingerprint
516     fingerprint=$(get_key_fingerprint "$keyID")
517
518     # attach a "non-exportable" signature to the key
519     # this is required for the key to have any validity at all
520     # the 'y's on stdin indicates "yes, i really want to sign"
521     echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
522
523     # import "full" trust for fingerprint into gpg
524     echo ${fingerprint}:5: | gpg --import-ownertrust
525     if [ $? = 0 ] ; then
526         log "owner trust updated."
527     else
528         failure "there was a problem changing owner trust."
529     fi  
530 }
531
532 # publish server key to keyserver
533 publish_server_key() {
534     read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
535     if [ ${OK/y/Y} != 'Y' ] ; then
536         failure "aborting."
537     fi
538
539     # publish host key
540     # FIXME: need to figure out better way to identify host key
541     # dummy command so as not to publish fakes keys during testing
542     # eventually:
543     #gpg --keyserver "$KEYSERVER" --send-keys $(hostname -f)
544     echo "NOT PUBLISHED (to avoid permanent publication errors during monkeysphere development).
545 To publish manually, do: gpg --keyserver $KEYSERVER --send-keys $(hostname -f)"
546     return 1
547 }