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