Fix bug in configuration handling for HASH_KNOWN_HOSTS and
[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 # managed directories
15 ETC="/etc/monkeysphere"
16 export ETC
17 CACHE="/var/cache/monkeysphere"
18 export CACHE
19 ########################################################################
20
21 failure() {
22     echo "$1" >&2
23     exit ${2:-'1'}
24 }
25
26 # write output to stderr
27 log() {
28     echo -n "ms: " 1>&2
29     echo "$@" 1>&2
30 }
31
32 # cut out all comments(#) and blank lines from standard input
33 meat() {
34     grep -v -e "^[[:space:]]*#" -e '^$'
35 }
36
37 # cut a specified line from standard input
38 cutline() {
39     head --line="$1" | tail -1
40 }
41
42 # retrieve all keys with given user id from keyserver
43 # FIXME: need to figure out how to retrieve all matching keys
44 # (not just first 5)
45 gpg_fetch_userid() {
46     local userID
47
48     userID="$1"
49
50     # if CHECK_KEYSERVER variable set, check the keyserver
51     # for the user ID
52     if [ "CHECK_KEYSERVER" ] ; then
53         echo 1,2,3,4,5 | \
54             gpg --quiet --batch --command-fd 0 --with-colons \
55             --keyserver "$KEYSERVER" \
56             --search ="$userID" >/dev/null 2>&1
57
58     # otherwise just return true
59     else
60         return
61     fi
62 }
63
64 # check that characters are in a string (in an AND fashion).
65 # used for checking key capability
66 # check_capability capability a [b...]
67 check_capability() {
68     local capability
69     local capcheck
70
71     capability="$1"
72     shift 1
73
74     for capcheck ; do
75         if echo "$capability" | grep -q -v "$capcheck" ; then
76             return 1
77         fi
78     done
79     return 0
80 }
81
82 # get the full fingerprint of a key ID
83 get_key_fingerprint() {
84     local keyID
85
86     keyID="$1"
87
88     gpg --list-key --with-colons --fixed-list-mode \
89         --with-fingerprint "$keyID" | grep "$keyID" | \
90         grep '^fpr:' | cut -d: -f10
91 }
92
93
94 # convert escaped characters from gpg output back into original
95 # character
96 # FIXME: undo all escape character translation in with-colons gpg output
97 unescape() {
98     echo "$1" | sed 's/\\x3a/:/'
99 }
100
101 # convert key from gpg to ssh known_hosts format
102 gpg2known_hosts() {
103     local keyID
104     local host
105
106     keyID="$1"
107     host=$(echo "$2" | sed -e "s|ssh://||")
108
109     # NOTE: it seems that ssh-keygen -R removes all comment fields from
110     # all lines in the known_hosts file.  why?
111     # NOTE: just in case, the COMMENT can be matched with the
112     # following regexp:
113     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
114     echo -n "$host "
115     gpg --export "$keyID" | \
116         openpgp2ssh "$keyID" | tr -d '\n'
117     echo " MonkeySphere${DATE}"
118 }
119
120 # convert key from gpg to ssh authorized_keys format
121 gpg2authorized_keys() {
122     local keyID
123     local userID
124
125     keyID="$1"
126     userID="$2"
127
128     gpg --export "$keyID" | \
129         openpgp2ssh "$keyID" | tr -d '\n'
130     echo " MonkeySphere${DATE}: ${userID}"
131 }
132
133 # userid and key policy checking
134 # the following checks policy on the returned keys
135 # - checks that full key has appropriate valididy (u|f)
136 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
137 # - checks that particular desired user id has appropriate validity
138 # see /usr/share/doc/gnupg/DETAILS.gz
139 # expects global variable: "MODE"
140 process_user_id() {
141     local userID
142     local cacheDir
143     local requiredCapability
144     local requiredPubCapability
145     local gpgOut
146     local line
147     local type
148     local validity
149     local keyid
150     local uidfpr
151     local capability
152     local keyOK
153     local pubKeyID
154     local uidOK
155     local keyIDs
156     local userIDHash
157     local keyID
158
159     userID="$1"
160     cacheDir="$2"
161
162     # set the required key capability based on the mode
163     if [ "$MODE" = 'known_hosts' ] ; then
164         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
165     elif [ "$MODE" = 'authorized_keys' ] ; then
166         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
167     fi
168     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
169
170     # fetch keys from keyserver, return 1 if none found
171     gpg_fetch_userid "$userID" || return 1
172
173     # output gpg info for (exact) userid and store
174     gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
175         ="$userID" 2> /dev/null)
176
177     # return 1 if there only "tru" lines are output from gpg
178     if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
179         log "  key not found."
180         return 1
181     fi
182
183     # loop over all lines in the gpg output and process.
184     # need to do it this way (as opposed to "while read...") so that
185     # variables set in loop will be visible outside of loop
186     for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do
187
188         # read the contents of the line
189         type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1)
190         validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2)
191         keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5)
192         uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10)
193         capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12)
194
195         # process based on record type
196         case $type in
197             'pub') # primary keys
198                 # new key, wipe the slate
199                 keyOK=
200                 pubKeyID=
201                 uidOK=
202                 keyIDs=
203
204                 pubKeyID="$keyid"
205
206                 # check primary key validity
207                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
208                     log "  unacceptable primary key validity ($validity)."
209                     continue
210                 fi
211                 # check capability is not Disabled...
212                 if check_capability "$capability" 'D' ; then
213                     log "  key disabled."
214                     continue
215                 fi
216                 # check overall key capability
217                 # must be Encryption and Authentication
218                 if ! check_capability "$capability" $requiredPubCapability ; then
219                     log "  unacceptable primary key capability ($capability)."
220                     continue
221                 fi
222
223                 # mark if primary key is acceptable
224                 keyOK=true
225
226                 # add primary key ID to key list if it has required capability
227                 if check_capability "$capability" $requiredCapability ; then
228                     keyIDs[${#keyIDs[*]}]="$keyid"
229                 fi
230                 ;;
231             'uid') # user ids
232                 # check key ok and we have key fingerprint
233                 if [ -z "$keyOK" ] ; then
234                     continue
235                 fi
236                 # check key validity
237                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
238                     continue
239                 fi
240                 # check the uid matches
241                 if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
242                     continue
243                 fi
244
245                 # mark if uid acceptable
246                 uidOK=true
247                 ;;
248             'sub') # sub keys
249                 # add sub key ID to key list if it has required capability
250                 if check_capability "$capability" $requiredCapability ; then
251                     keyIDs[${#keyIDs[*]}]="$keyid"
252                 fi
253                 ;;
254         esac
255     done
256
257     # hash userid for cache file name
258     userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }')
259
260     # make sure the cache directory exists
261     mkdir -p "$cacheDir"
262
263     # touch/clear key cache file
264     # (will be left empty if there are noacceptable keys)
265     > "$cacheDir"/"$userIDHash"."$pubKeyID"
266
267     # for each acceptable key, write an ssh key line to the
268     # key cache file
269     if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
270         for keyID in ${keyIDs[@]} ; do
271             log "  acceptable key/uid found."
272
273             if [ "$MODE" = 'known_hosts' ] ; then
274                 # export the key
275                 gpg2known_hosts "$keyID" "$userID" >> \
276                     "$cacheDir"/"$userIDHash"."$pubKeyID"
277                 # hash the cache file if specified
278                 if [ "$HASH_KNOWN_HOSTS" = "true" ] ; then
279                     ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
280                     rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
281                 fi
282             elif [ "$MODE" = 'authorized_keys' ] ; then
283                 # export the key
284                 # FIXME: needs to apply extra options for authorized_keys
285                 # lines if specified
286                 gpg2authorized_keys "$keyID" "$userID" >> \
287                     "$cacheDir"/"$userIDHash"."$pubKeyID"
288             fi
289         done
290     fi
291
292     # echo the path to the key cache file
293     echo "$cacheDir"/"$userIDHash"."$pubKeyID"
294 }
295
296 # update the cache for userid, and prompt to add file to
297 # authorized_user_ids file if the userid is found in gpg
298 # and not already in file.
299 update_userid() {
300     local userID
301     local cacheDir
302     local keyCache
303
304     userID="$1"
305     cacheDir="$2"
306
307     log "processing userid: '$userID'"
308
309     # return 1 if there is no output of the user ID processing
310     # ie. no key was found
311     keyCachePath=$(process_user_id "$userID" "$cacheDir")
312     if [ -z "$keyCachePath" ] ; then
313         return 1
314     fi
315
316     # check if user ID is in the authorized_user_ids file
317     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
318         read -p "user ID not currently authorized.  authorize? [Y|n]: " OK; OK=${OK:=Y}
319         if [ ${OK/y/Y} = 'Y' ] ; then
320             # add if specified
321             log -n "adding user ID to authorized_user_ids file... "
322             echo "$userID" >> "$AUTHORIZED_USER_IDS"
323             echo "done."
324         else
325             # else do nothing
326             log "authorized_user_ids file untouched."
327         fi
328     fi
329 }
330
331 # remove a userid from the authorized_user_ids file
332 remove_userid() {
333     local userID
334
335     userID="$1"
336
337     log "processing userid: '$userID'"
338
339     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
340         log "user ID not currently authorized."
341         return 1
342     fi
343
344     log -n "removing user ID '$userID'... "
345     grep -v "$userID" "$AUTHORIZED_USER_IDS" | sponge "$AUTHORIZED_USER_IDS"
346     echo "done."
347 }
348
349 # process a host for addition to a known_host file
350 process_host() {
351     local host
352     local cacheDir
353     local keyCachePath
354
355     host="$1"
356     cacheDir="$2"
357
358     log "processing host: $host"
359
360     keyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
361     if [ $? = 0 ] ; then
362         ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
363         cat "$keyCachePath" >> "$USER_KNOWN_HOSTS"
364     fi
365 }
366
367 # process known_hosts file
368 # go through line-by-line, extract each host, and process with the
369 # host processing function
370 process_known_hosts() {
371     local cacheDir
372     local hosts
373     local host
374
375     cacheDir="$1"
376
377     # take all the hosts from the known_hosts file (first field),
378     # grep out all the hashed hosts (lines starting with '|')...
379     meat "$USER_KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | \
380     while IFS=, read -r -a hosts ; do
381         # ...and process each host
382         for host in ${hosts[*]} ; do
383             process_host "$host" "$cacheDir"
384         done
385     done
386 }
387
388 # update an authorized_keys file after first processing the 
389 # authorized_user_ids file
390 update_authorized_keys() {
391     local msAuthorizedKeys
392     local userAuthorizedKeys
393     local cacheDir
394
395     msAuthorizedKeys="$1"
396     userAuthorizedKeys="$2"
397     cacheDir="$3"
398
399     process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
400
401     # write output key file
402     log "writing monkeysphere authorized_keys file... "
403     touch "$msAuthorizedKeys"
404     if [ "$(ls "$cacheDir")" ] ; then
405         log -n "adding gpg keys... "
406         cat "$cacheDir"/* > "$msAuthorizedKeys"
407         echo "done."
408     else
409         log "no gpg keys to add."
410     fi
411     if [ "$userAuthorizedKeys" != "-" -a -s "$userAuthorizedKeys" ] ; then
412         log -n "adding user authorized_keys file... "
413         cat "$userAuthorizedKeys" >> "$msAuthorizedKeys"
414         echo "done."
415     fi
416     log "monkeysphere authorized_keys file generated:"
417     log "$msAuthorizedKeys"
418 }
419
420 # process an authorized_*_ids file
421 # go through line-by-line, extract each userid, and process
422 process_authorized_ids() {
423     local authorizedIDs
424     local cacheDir
425     local userID
426
427     authorizedIDs="$1"
428     cacheDir="$2"
429
430     # clean out keys file and remake keys directory
431     rm -rf "$cacheDir"
432     mkdir -p "$cacheDir"
433
434     # loop through all user ids in file
435     # FIXME: needs to handle authorized_keys options
436     cat "$authorizedIDs" | meat | \
437     while read -r userID ; do
438         # process the userid
439         log "processing userid: '$userID'"
440         process_user_id "$userID" "$cacheDir" > /dev/null
441     done
442 }
443
444 # EXPERIMENTAL (unused) process userids found in authorized_keys file
445 # go through line-by-line, extract monkeysphere userids from comment
446 # fields, and process each userid
447 process_authorized_keys() {
448     local authorizedKeys
449     local cacheDir
450     local userID
451
452     authorizedKeys="$1"
453     cacheDir="$2"
454
455     # take all the monkeysphere userids from the authorized_keys file
456     # comment field (third field) that starts with "MonkeySphere uid:"
457     # FIXME: needs to handle authorized_keys options (field 0)
458     cat "$authorizedKeys" | \
459     while read -r options keytype key comment ; do
460         # if the comment field is empty, assume the third field was
461         # the comment
462         if [ -z "$comment" ] ; then
463             comment="$key"
464         fi
465         if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
466             continue
467         fi
468         userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
469         if [ -z "$userID" ] ; then
470             continue
471         fi
472         # process the userid
473         log "processing userid: '$userID'"
474         process_user_id "$userID" "$cacheDir" > /dev/null
475     done
476 }
477
478 # retrieve key from web of trust, and set owner trust to "full"
479 # if key is found.
480 trust_key() {
481     # get the key from the key server
482     if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
483         log "could not retrieve key '$keyID'"
484         return 1
485     fi
486
487     # get key fingerprint
488     fingerprint=$(get_key_fingerprint "$keyID")
489
490     # attach a "non-exportable" signature to the key
491     # this is required for the key to have any validity at all
492     # the 'y's on stdin indicates "yes, i really want to sign"
493     echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
494
495     # import "full" trust for fingerprint into gpg
496     echo ${fingerprint}:5: | gpg --import-ownertrust
497     if [ $? = 0 ] ; then
498         log "owner trust updated."
499     else
500         failure "there was a problem changing owner trust."
501     fi  
502 }
503
504 # publish server key to keyserver
505 publish_server_key() {
506     read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
507     if [ ${OK/y/Y} != 'Y' ] ; then
508         failure "aborting."
509     fi
510
511     # publish host key
512     # FIXME: need to figure out better way to identify host key
513     # dummy command so as not to publish fakes keys during testing
514     # eventually:
515     #gpg --send-keys --keyserver "$KEYSERVER" $(hostname -f)
516     echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)"
517 }