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