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