8643080f595a67aee3f6e46aa1e4922a8a9bc2e6
[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="$2"
92     userID="$3"
93
94     if [ "$mode" = 'authorized_keys' ] ; then
95         gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${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 # process authorized_keys file
298 # go through line-by-line, extract monkeysphere userids from comment
299 # fields, and process each userid
300 process_authorized_keys() {
301     local authorizedKeys
302     local cacheDir
303     local userID
304
305     authorizedKeys="$1"
306     cacheDir="$2"
307
308     # take all the monkeysphere userids from the authorized_keys file
309     # comment field (third field) that starts with "MonkeySphere uid:"
310     # FIXME: needs to handle authorized_keys options (field 0)
311     cat "$authorizedKeys" | \
312     while read -r options keytype key comment ; do
313         # if the comment field is empty, assume the third field was
314         # the comment
315         if [ -z "$comment" ] ; then
316             comment="$key"
317         fi
318         if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
319             continue
320         fi
321         userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
322         if [ -z "$userID" ] ; then
323             continue
324         fi
325         # process the userid
326         log "processing userid: '$userID'"
327         process_user_id "$userID" "$cacheDir" > /dev/null
328     done
329 }
330
331 # process an authorized_*_ids file
332 # go through line-by-line, extract each userid, and process
333 process_authorized_ids() {
334     local authorizedIDs
335     local cacheDir
336     local userID
337
338     authorizedIDs="$1"
339     cacheDir="$2"
340
341     # clean out keys file and remake keys directory
342     rm -rf "$cacheDir"
343     mkdir -p "$cacheDir"
344
345     # loop through all user ids in file
346     # FIXME: needs to handle authorized_keys options
347     cat "$authorizedIDs" | meat | \
348     while read -r userID ; do
349         # process the userid
350         log "processing userid: '$userID'"
351         process_user_id "$userID" "$cacheDir" > /dev/null
352     done
353 }