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