More cleanup of scripts
[monkeysphere.git] / src / common
1 # -*-shell-script-*-
2
3 # Shared bash 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 LIB="/var/lib/monkeysphere"
18 export LIB 
19 ########################################################################
20
21 failure() {
22     echo "$1" >&2
23     exit ${2:-'1'}
24 }
25
26 # write output to stdout
27 log() {
28     echo -n "ms: "
29     echo "$@"
30 }
31
32 # write output to stderr
33 loge() {
34     echo -n "ms: " 1>&2
35     echo "$@" 1>&2
36 }
37
38 # cut out all comments(#) and blank lines from standard input
39 meat() {
40     grep -v -e "^[[:space:]]*#" -e '^$'
41 }
42
43 # cut a specified line from standard input
44 cutline() {
45     head --line="$1" | tail -1
46 }
47
48 # retrieve all keys with given user id from keyserver
49 # FIXME: need to figure out how to retrieve all matching keys
50 # (not just first 5)
51 gpg_fetch_keys() {
52     local id
53     id="$1"
54     echo 1,2,3,4,5 | \
55         gpg --quiet --batch --command-fd 0 --with-colons \
56         --keyserver "$KEYSERVER" \
57         --search ="$id" >/dev/null 2>&1
58 }
59
60 # check that characters are in a string (in an AND fashion).
61 # used for checking key capability
62 # check_capability capability a [b...]
63 check_capability() {
64     local capability
65     local capcheck
66
67     capability="$1"
68     shift 1
69
70     for capcheck ; do
71         if echo "$capability" | grep -q -v "$capcheck" ; then
72             return 1
73         fi
74     done
75     return 0
76 }
77
78 # convert escaped characters from gpg output back into original
79 # character
80 # FIXME: undo all escape character translation in with-colons gpg output
81 unescape() {
82     echo "$1" | sed 's/\\x3a/:/'
83 }
84
85 # stand in until we get dkg's gpg2ssh program
86 gpg2ssh_tmp() {
87     local keyID
88     local userID
89     local host
90
91     keyID="$1"
92     userID="$2"
93
94     if [ "$MODE" = 'authorized_keys' ] ; then
95         gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere userID: ${userID}/"
96
97     # NOTE: it seems that ssh-keygen -R removes all comment fields from
98     # all lines in the known_hosts file.  why?
99     # NOTE: just in case, the COMMENT can be matched with the
100     # following regexp:
101     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
102     elif [ "$MODE" = 'known_hosts' ] ; then
103         host=$(echo "$userID" | sed -e "s|ssh://||")
104         echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/"
105     fi
106 }
107
108 # userid and key policy checking
109 # the following checks policy on the returned keys
110 # - checks that full key has appropriate valididy (u|f)
111 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
112 # - checks that particular desired user id has appropriate validity
113 # see /usr/share/doc/gnupg/DETAILS.gz
114 # expects global variable: "MODE"
115 process_user_id() {
116     local userID
117     local cacheDir
118     local requiredPubCapability
119     local gpgOut
120     local line
121     local type
122     local validity
123     local keyid
124     local uidfpr
125     local capability
126     local keyOK
127     local pubKeyID
128     local uidOK
129     local keyIDs
130     local userIDHash
131     local keyID
132
133     userID="$1"
134     cacheDir="$2"
135
136     requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]")
137
138     # fetch keys from keyserver, return 1 if none found
139     gpg_fetch_keys "$userID" || return 1
140
141     # output gpg info for (exact) userid and store
142     gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
143         ="$userID" 2> /dev/null)
144
145     # return 1 if there only "tru" lines are output from gpg
146     if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
147         loge "  key not found."
148         return 1
149     fi
150
151     # loop over all lines in the gpg output and process.
152     # need to do it this way (as opposed to "while read...") so that
153     # variables set in loop will be visible outside of loop
154     for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do
155
156         # read the contents of the line
157         type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1)
158         validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2)
159         keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5)
160         uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10)
161         capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12)
162
163         # process based on record type
164         case $type in
165             'pub') # primary keys
166                 # new key, wipe the slate
167                 keyOK=
168                 pubKeyID=
169                 uidOK=
170                 keyIDs=
171
172                 pubKeyID="$keyid"
173
174                 # check primary key validity
175                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
176                     loge "  unacceptable primary key validity ($validity)."
177                     continue
178                 fi
179                 # check capability is not Disabled...
180                 if check_capability "$capability" 'D' ; then
181                     loge "  key disabled."
182                     continue
183                 fi
184                 # check overall key capability
185                 # must be Encryption and Authentication
186                 if ! check_capability "$capability" $requiredPubCapability ; then
187                     loge "  unacceptable primary key capability ($capability)."
188                     continue
189                 fi
190
191                 # mark if primary key is acceptable
192                 keyOK=true
193
194                 # add primary key ID to key list if it has required capability
195                 if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
196                     keyIDs[${#keyIDs[*]}]="$keyid"
197                 fi
198                 ;;
199             'uid') # user ids
200                 # check key ok and we have key fingerprint
201                 if [ -z "$keyOK" ] ; then
202                     continue
203                 fi
204                 # check key validity
205                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
206                     continue
207                 fi
208                 # check the uid matches
209                 if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
210                     continue
211                 fi
212
213                 # mark if uid acceptable
214                 uidOK=true
215                 ;;
216             'sub') # sub keys
217                 # add sub key ID to key list if it has required capability
218                 if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
219                     keyIDs[${#keyIDs[*]}]="$keyid"
220                 fi
221                 ;;
222         esac
223     done
224
225     # hash userid for cache file name
226     userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }')
227
228     # touch/clear key cache file
229     # (will be left empty if there are noacceptable keys)
230     > "$cacheDir"/"$userIDHash"."$pubKeyID"
231
232     # for each acceptable key, write an ssh key line to the
233     # key cache file
234     if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
235         for keyID in ${keyIDs[@]} ; do
236             loge "  acceptable key/uid found."
237
238             # export the key with gpg2ssh
239             # FIXME: needs to apply extra options for authorized_keys
240             # lines if specified
241             gpg2ssh_tmp "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID"
242
243             # hash the cache file if specified
244             if [ "$MODE" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then
245                 ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
246                 rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
247             fi
248         done
249     fi
250
251     # echo the path to the key cache file
252     echo "$cacheDir"/"$userIDHash"."$pubKeyID"
253 }
254
255 # process a host for addition to a known_host file
256 process_host() {
257     local host
258     local cacheDir
259     local hostKeyCachePath
260
261     host="$1"
262     cacheDir="$2"
263
264     log "processing host: '$host'"
265
266     hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
267     if [ $? = 0 ] ; then
268         ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
269         cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS"
270     fi
271 }
272
273 # process known_hosts file
274 # go through line-by-line, extract each host, and process with the
275 # host processing function
276 process_known_hosts() {
277     local knownHosts
278     local cacheDir
279     local hosts
280     local host
281
282     knownHosts="$1"
283     cacheDir="$2"
284
285     # take all the hosts from the known_hosts file (first field),
286     # grep out all the hashed hosts (lines starting with '|')
287     cut -d ' ' -f 1 "$knownHosts" | \
288     grep -v '^|.*$' | \
289     while IFS=, read -r -a hosts ; do
290         # process each host
291         for host in ${hosts[*]} ; do
292             process_host "$host" "$cacheDir"
293         done
294     done
295 }
296
297 # update an authorized_keys file after first processing the 
298 # authorized_user_ids file
299 update_authorized_keys() {
300     local cacheDir
301     local msAuthorizedKeys
302     local userAuthorizedKeys
303
304     cacheDir="$1"
305     msAuthorizedKeys="$2"
306     userAuthorizedKeys="$3"
307
308     process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
309
310     # write output key file
311     log "writing monkeysphere authorized_keys file... "
312     touch "$msAuthorizedKeys"
313     if [ "$(ls "$cacheDir")" ] ; then
314         log -n "adding gpg keys... "
315         cat "$cacheDir"/* > "$msAuthorizedKeys"
316         echo "done."
317     else
318         log "no gpg keys to add."
319     fi
320     if [ "$userAuthorizedKeys" -a -s "$userAuthorizedKeys" ] ; then
321         log -n "adding user authorized_keys file... "
322         cat "$userAuthorizedKeys" >> "$msAuthorizedKeys"
323         echo "done."
324     fi
325     log "monkeysphere authorized_keys file generated: $msAuthorizedKeys"
326 }
327
328 # process an authorized_*_ids file
329 # go through line-by-line, extract each userid, and process
330 process_authorized_ids() {
331     local authorizedIDs
332     local cacheDir
333     local userID
334
335     authorizedIDs="$1"
336     cacheDir="$2"
337
338     # clean out keys file and remake keys directory
339     rm -rf "$cacheDir"
340     mkdir -p "$cacheDir"
341
342     # loop through all user ids in file
343     # FIXME: needs to handle authorized_keys options
344     cat "$authorizedIDs" | meat | \
345     while read -r userID ; do
346         # process the userid
347         log "processing userid: '$userID'"
348         process_user_id "$userID" "$cacheDir" > /dev/null
349     done
350 }
351
352 # EXPERIMENTAL (unused) process userids found in authorized_keys file
353 # go through line-by-line, extract monkeysphere userids from comment
354 # fields, and process each userid
355 process_userids_from_authorized_keys() {
356     local authorizedKeys
357     local cacheDir
358     local userID
359
360     authorizedKeys="$1"
361     cacheDir="$2"
362
363     # take all the monkeysphere userids from the authorized_keys file
364     # comment field (third field) that starts with "MonkeySphere uid:"
365     # FIXME: needs to handle authorized_keys options (field 0)
366     cat "$authorizedKeys" | \
367     while read -r options keytype key comment ; do
368         # if the comment field is empty, assume the third field was
369         # the comment
370         if [ -z "$comment" ] ; then
371             comment="$key"
372         fi
373         if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
374             continue
375         fi
376         userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
377         if [ -z "$userID" ] ; then
378             continue
379         fi
380         # process the userid
381         log "processing userid: '$userID'"
382         process_user_id "$userID" "$cacheDir" > /dev/null
383     done
384 }
385
386 # update the cache for userid, and prompt to add file to
387 # authorized_user_ids file if the userid is found in gpg
388 # and not already in file.
389 update_userid() {
390     local userID
391     local cacheDir
392     local userIDKeyCache
393
394     userID="$1"
395     cacheDir="$2"
396
397     log "processing userid: '$userID'"
398     userIDKeyCache=$(process_user_id "$userID" "$cacheDir")
399     if [ -z "$userIDKeyCache" ] ; then
400         return 1
401     fi
402     if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
403         echo "the following userid is not in the authorized_user_ids file:"
404         echo "  $userID"
405         read -p "would you like to add? [Y|n]: " OK; OK=${OK:=Y}
406         if [ ${OK/y/Y} = 'Y' ] ; then
407             log -n "  adding userid to authorized_user_ids file... "
408             echo "$userID" >> "$AUTHORIZED_USER_IDS"
409             echo "done."
410         fi
411     fi
412 }
413
414 # retrieve key from web of trust, and set owner trust to "full"
415 # if key is found.
416 trust_key() {
417     # get the key from the key server
418     gpg --keyserver "$KEYSERVER" --recv-key "$keyID" || failure "could not retrieve key '$keyID'"
419
420     # edit the key to change trust
421     # FIXME: need to figure out how to automate this,
422     # in a batch mode or something.
423     gpg --edit-key "$keyID"
424 }