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