64d28cb221871c351d798c31d74f908bcb3b8265
[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     log "checking keyserver $KEYSERVER..."
51     echo 1,2,3,4,5 | \
52         gpg --quiet --batch --command-fd 0 --with-colons \
53         --keyserver "$KEYSERVER" \
54         --search ="$userID" >/dev/null 2>&1
55     if [ "$?" = 0 ] ; then
56         log "  user ID found on keyserver."
57         return 0
58     else
59         log "  user ID not found on keyserver."
60         return 1
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 userIDHash
147     local keyCacheDir
148     local line
149     local type
150     local validity
151     local keyid
152     local uidfpr
153     local usage
154     local keyOK
155     local pubKeyID
156     local uidOK
157     local keyIDs
158     local keyID
159
160     userID="$1"
161     cacheDir="$2"
162
163     # set the required key capability based on the mode
164     if [ "$MODE" = 'known_hosts' ] ; then
165         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
166     elif [ "$MODE" = 'authorized_keys' ] ; then
167         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
168     fi
169     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
170
171     # if CHECK_KEYSERVER variable set, check the keyserver
172     # for the user ID
173     if [ "$CHECK_KEYSERVER" = "true" ] ; then
174         gpg_fetch_userid "$userID"
175     fi
176
177     # output gpg info for (exact) userid and store
178     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
179         --with-fingerprint --with-fingerprint \
180         ="$userID" 2>/dev/null)
181
182     # if the gpg query return code is not 0, return 1
183     if [ "$?" -ne 0 ] ; then
184         log "  key not found."
185         return 1
186     fi
187
188     echo "$gpgOut"
189
190     # loop over all lines in the gpg output and process.
191     # need to do it this way (as opposed to "while read...") so that
192     # variables set in loop will be visible outside of loop
193     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
194     while IFS=: read -r type validity keyid uidfpr usage ; do
195         # process based on record type
196         case $type in
197             'pub') # primary keys
198                 # new key, wipe the slate
199                 keyOK=
200                 uidOK=
201                 pubKeyOK=
202                 fingerprint=
203
204                 # if overall key is not valid, skip
205                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
206                     log "  unacceptable primary key validity ($validity)."
207                     continue
208                 fi
209                 # if overall key is disabled, skip
210                 if check_capability "$usage" 'D' ; then
211                     log "  key disabled."
212                     continue
213                 fi
214                 # if overall key capability is not ok, skip
215                 if ! check_capability "$usage" $requiredPubCapability ; then
216                     log "  unacceptable primary key capability ($usage)."
217                     continue
218                 fi
219
220                 # mark overall key as ok
221                 keyOK=true
222
223                 # mark primary key as ok if capability is ok
224                 if check_capability "$usage" $requiredCapability ; then
225                     pubKeyOK=true
226                 fi
227                 ;;
228             'uid') # user ids
229                 # if the overall key is not ok, skip
230                 if [ -z "$keyOK" ] ; then
231                     continue
232                 fi
233                 # if an acceptable user ID was already found, skip
234                 if [ "$uidOK" ] ; then
235                     continue
236                 fi
237                 # if the user ID does not match, skip
238                 if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
239                     continue
240                 fi
241                 # if the user ID validity is not ok, skip
242                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
243                     continue
244                 fi
245
246                 # mark user ID acceptable
247                 uidOK=true
248
249                 # output a line for the primary key
250                 # 0 = ok, 1 = bad
251                 if [ "$keyOK" -a "$uidOK" -a "$pubKeyOK" ] ; then
252                     log "  acceptable key found"
253                     echo 0 "$fingerprint"
254                 else
255                     echo 1 "$fingerprint"
256                 fi
257                 ;;
258             'sub') # sub keys
259                 # unset acceptability of last key
260                 subKeyOK=
261                 fingerprint=
262
263                 # if the overall key is not ok, skip
264                 if [ -z "$keyOK" ] ; then
265                     continue
266                 fi
267                 # if sub key validity is not ok, skip
268                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
269                     continue
270                 fi
271                 # if sub key capability is not ok, skip
272                 if ! check_capability "$usage" $requiredCapability ; then
273                     continue
274                 fi
275
276                 # mark sub key as ok
277                 subKeyOK=true
278                 ;;
279             'fpr') # key fingerprint
280                 fingerprint="$uidfpr"
281
282                 # output a line for the last subkey
283                 # 0 = ok, 1 = bad
284                 if [ "$keyOK" -a "$uidOK" -a "$subKeyOK" ] ; then
285                     log "  acceptable key found"
286                     echo 0 "$fingerprint"
287                 else
288                     echo 1 "$fingerprint"
289                 fi
290                 ;;
291         esac
292     done
293 }
294
295 # update the cache for userid, and prompt to add file to
296 # authorized_user_ids file if the userid is found in gpg
297 # and not already in file.
298 update_userid() {
299     local userID
300     local cacheDir
301     local keyCache
302
303     userID="$1"
304     cacheDir="$2"
305
306     log "processing userid: '$userID'"
307
308     # return 1 if there is no output of the user ID processing
309     # ie. no key was found
310     keyCachePath=$(process_user_id "$userID" "$cacheDir")
311     if [ -z "$keyCachePath" ] ; then
312         return 1
313     fi
314
315     # check if user ID is in the authorized_user_ids file
316     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
317         read -p "user ID not currently authorized.  authorize? [Y|n]: " OK; OK=${OK:=Y}
318         if [ ${OK/y/Y} = 'Y' ] ; then
319             # add if specified
320             log -n "adding user ID to authorized_user_ids file... "
321             echo "$userID" >> "$AUTHORIZED_USER_IDS"
322             echo "done."
323         else
324             # else do nothing
325             log "authorized_user_ids file untouched."
326         fi
327     fi
328 }
329
330 # remove a userid from the authorized_user_ids file
331 remove_userid() {
332     local userID
333
334     userID="$1"
335
336     log "processing userid: '$userID'"
337
338     # check if user ID is in the authorized_user_ids file
339     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
340         log "user ID not currently authorized."
341         return 1
342     fi
343
344     # remove user ID from file
345     log -n "removing user ID '$userID'... "
346     grep -v "$userID" "$AUTHORIZED_USER_IDS" | sponge "$AUTHORIZED_USER_IDS"
347     echo "done."
348 }
349
350 # remove all keys from specified key cache from known_hosts file
351 remove_known_hosts_host_keys() {
352     local keyCachePath
353     local hosts
354     local type
355     local key
356     local comment
357
358     keyCachePath="$1"
359
360     meat "${keyCachePath}/keys" | \
361     while read -r hosts type key comment ; do
362         grep -v "$key" "$USER_KNOWN_HOSTS" | sponge "$USER_KNOWN_HOSTS"
363     done
364 }
365
366 # process a host for addition to a known_host file
367 process_host() {
368     local host
369     local cacheDir
370     local keyCachePath
371
372     host="$1"
373     cacheDir="$2"
374
375     log "processing host: $host"
376
377     userID="ssh://${host}"
378     process_user_id "ssh://${host}"
379     exit
380     process_user_id "ssh://${host}" | \
381     while read -r ok key ; do
382         # remove the old host key line
383         remove_known_hosts_host_keys "$key"
384         # if key OK, add new host line
385         if [ "$ok" -eq '0' ] ; then
386             known_hosts_line "$host" "$key" >> "$USER_KNOWN_HOSTS"
387         fi
388     done
389 }
390
391 # process known_hosts file
392 # go through line-by-line, extract each host, and process with the
393 # host processing function
394 process_known_hosts() {
395     local cacheDir
396     local hosts
397     local host
398
399     cacheDir="$1"
400
401     # take all the hosts from the known_hosts file (first field),
402     # grep out all the hashed hosts (lines starting with '|')...
403     meat "$USER_KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | \
404     while IFS=, read -r -a hosts ; do
405         # ...and process each host
406         for host in ${hosts[*]} ; do
407             process_host "$host" "$cacheDir"
408         done
409     done
410 }
411
412 # update an authorized_keys file after first processing the 
413 # authorized_user_ids file
414 update_authorized_keys() {
415     local msAuthorizedKeys
416     local userAuthorizedKeys
417     local cacheDir
418
419     msAuthorizedKeys="$1"
420     userAuthorizedKeys="$2"
421     cacheDir="$3"
422
423     process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
424
425     # write output key file
426     log "writing monkeysphere authorized_keys file... "
427     touch "$msAuthorizedKeys"
428     if [ "$(ls "$cacheDir")" ] ; then
429         log -n "adding gpg keys... "
430         cat "$cacheDir"/* > "$msAuthorizedKeys"
431         echo "done."
432     else
433         log "no gpg keys to add."
434     fi
435     if [ "$userAuthorizedKeys" != "-" -a -s "$userAuthorizedKeys" ] ; then
436         log -n "adding user authorized_keys file... "
437         cat "$userAuthorizedKeys" >> "$msAuthorizedKeys"
438         echo "done."
439     fi
440     log "monkeysphere authorized_keys file generated:"
441     log "$msAuthorizedKeys"
442 }
443
444 # process an authorized_*_ids file
445 # go through line-by-line, extract each userid, and process
446 process_authorized_ids() {
447     local authorizedIDs
448     local cacheDir
449     local userID
450
451     authorizedIDs="$1"
452     cacheDir="$2"
453
454     process_user_id "$userID" | \
455     while read -r ok key ; do
456         # remove the old host key line
457         remove_authorized_keys_user_keys "$key"
458         # if key OK, add new host line
459         if [ "$ok" -eq '0' ] ; then
460             authorized_keys_line "$userID" "$key" >> "$USER_AUTHORIZED_KEYS"
461         fi
462     done
463 }
464
465 # EXPERIMENTAL (unused) process userids found in authorized_keys file
466 # go through line-by-line, extract monkeysphere userids from comment
467 # fields, and process each userid
468 process_authorized_keys() {
469     local authorizedKeys
470     local cacheDir
471     local userID
472
473     authorizedKeys="$1"
474     cacheDir="$2"
475
476     # take all the monkeysphere userids from the authorized_keys file
477     # comment field (third field) that starts with "MonkeySphere uid:"
478     # FIXME: needs to handle authorized_keys options (field 0)
479     cat "$authorizedKeys" | \
480     while read -r options keytype key comment ; do
481         # if the comment field is empty, assume the third field was
482         # the comment
483         if [ -z "$comment" ] ; then
484             comment="$key"
485         fi
486         if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
487             continue
488         fi
489         userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
490         if [ -z "$userID" ] ; then
491             continue
492         fi
493         # process the userid
494         log "processing userid: '$userID'"
495         process_user_id "$userID" "$cacheDir" > /dev/null
496     done
497 }
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 --send-keys --keyserver "$KEYSERVER" $(hostname -f)
537     echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)"
538 }