3 # rhesus: monkeysphere authorized_keys/known_hosts generating script
6 # Jameson Rollins <jrollins@fifthhorseman.net>
8 # Copyright 2008, released under the GPL, version 3 or later
10 # all caps variables are meant to be user supplied (ie. from config
11 # file) and are considered global
15 # date in UTF format if needed
16 DATE=$(date -u '+%FT%T')
18 # unset some environment variables that could screw things up
21 ########################################################################
23 ########################################################################
27 usage: $PGRM k|known_hosts [host...]
28 $PGRM a|authorized_keys [userid...]
29 Monkeysphere update of known_hosts or authorized_keys file.
30 If hosts/userids are specified, only those specified will be processed
39 # write output to stdout
45 # write output to stderr
51 # cut out all comments(#) and blank lines from standard input
53 grep -v -e "^[[:space:]]*#" -e '^$'
56 # cut a specified line from standard input
58 head --line="$1" | tail -1
61 # retrieve all keys with given user id from keyserver
62 # FIXME: need to figure out how to retrieve all matching keys
68 gpg --quiet --batch --command-fd 0 --with-colons \
69 --keyserver "$KEYSERVER" \
70 --search ="$id" >/dev/null 2>&1
73 # check that characters are in a string (in an AND fashion).
74 # used for checking key capability
75 # check_capability capability a [b...]
84 if echo "$capability" | grep -q -v "$capcheck" ; then
91 # convert escaped characters from gpg output back into original
93 # FIXME: undo all escape character translation in with-colons gpg output
95 echo "$1" | sed 's/\\x3a/:/'
98 # stand in until we get dkg's gpg2ssh program
109 if [ "$mode" = 'authorized_keys' ] ; then
110 gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${userID}/"
112 # NOTE: it seems that ssh-keygen -R removes all comment fields from
113 # all lines in the known_hosts file. why?
114 # NOTE: just in case, the COMMENT can be matched with the
116 # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
117 elif [ "$mode" = 'known_hosts' ] ; then
118 host=$(echo "$userID" | sed -e "s|ssh://||")
119 echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/"
123 # userid and key policy checking
124 # the following checks policy on the returned keys
125 # - checks that full key has appropriate valididy (u|f)
126 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
127 # - checks that particular desired user id has appropriate validity
128 # see /usr/share/doc/gnupg/DETAILS.gz
129 # expects global variable: "mode"
133 local requiredPubCapability
151 requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]")
153 # fetch keys from keyserver, return 1 if none found
154 gpg_fetch_keys "$userID" || return 1
156 # output gpg info for (exact) userid and store
157 gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
158 ="$userID" 2> /dev/null)
160 # return 1 if there only "tru" lines are output from gpg
161 if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
165 # loop over all lines in the gpg output and process.
166 # need to do it this way (as opposed to "while read...") so that
167 # variables set in loop will be visible outside of loop
168 for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do
170 # read the contents of the line
171 type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1)
172 validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2)
173 keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5)
174 uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10)
175 capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12)
177 # process based on record type
179 'pub') # primary keys
180 # new key, wipe the slate
188 # check primary key validity
189 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
190 loge " unacceptable primary key validity ($validity)."
193 # check capability is not Disabled...
194 if check_capability "$capability" 'D' ; then
195 loge " key disabled."
198 # check overall key capability
199 # must be Encryption and Authentication
200 if ! check_capability "$capability" $requiredPubCapability ; then
201 loge " unacceptable primary key capability ($capability)."
205 # mark if primary key is acceptable
208 # add primary key ID to key list if it has required capability
209 if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
210 keyIDs[${#keyIDs[*]}]="$keyid"
214 # check key ok and we have key fingerprint
215 if [ -z "$keyOK" ] ; then
219 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
222 # check the uid matches
223 if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
227 # mark if uid acceptable
231 # add sub key ID to key list if it has required capability
232 if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
233 keyIDs[${#keyIDs[*]}]="$keyid"
239 # hash userid for cache file name
240 userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }')
242 # touch/clear key cache file
243 # (will be left empty if there are noacceptable keys)
244 > "$cacheDir"/"$userIDHash"."$pubKeyID"
246 # for each acceptable key, write an ssh key line to the
248 if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
249 for keyID in ${keyIDs[@]} ; do
250 # export the key with gpg2ssh
251 # FIXME: needs to apply extra options for authorized_keys
253 gpg2ssh_tmp "$mode" "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID"
255 # hash the cache file if specified
256 if [ "$mode" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then
257 ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
258 rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
263 # echo the path to the key cache file
264 echo "$cacheDir"/"$userIDHash"."$pubKeyID"
267 # process a host for addition to a known_host file
271 local hostKeyCachePath
276 log "processing host: '$host'"
278 hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
280 ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
281 cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS"
285 # process known_hosts file
286 # go through line-by-line, extract each host, and process with the
287 # host processing function
288 process_known_hosts() {
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 "$USER_KNOWN_HOSTS" | \
298 while IFS=, read -r -a hosts ; do
300 for host in ${hosts[*]} ; do
301 process_host "$host" "$cacheDir"
306 # process an authorized_*_ids file
307 # go through line-by-line, extract each userid, and process
308 process_authorized_ids() {
309 local authorizedIDsFile
312 local userKeyCachePath
314 authorizedIDsFile="$1"
317 # clean out keys file and remake keys directory
321 # loop through all user ids in file
322 # FIXME: needs to handle extra options if necessary
323 cat "$authorizedIDsFile" | meat | \
324 while read -r userID ; do
326 log "processing userid: '$userID'"
327 userKeyCachePath=$(process_user_id "$userID" "$cacheDir")
328 if [ -s "$userKeyCachePath" ] ; then
329 loge " acceptable key/uid found."
334 ########################################################################
336 ########################################################################
338 if [ -z "$1" ] ; then
343 # mode given in first variable
348 if ! id -u "$USER" > /dev/null 2>&1 ; then
349 failure "invalid user '$USER'."
352 # set user home directory
353 HOME=$(getent passwd "$USER" | cut -d: -f6)
355 # set ms home directory
356 MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere}
358 # load configuration file
359 MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf}
360 [ -e "$MS_CONF" ] && . "$MS_CONF"
362 # set config variable defaults
363 STAGING_AREA=${STAGING_AREA:-"$MS_HOME"}
364 AUTHORIZED_USER_IDS=${AUTHORIZED_USER_IDS:-"$MS_HOME"/authorized_user_ids}
365 GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg}
366 KEYSERVER=${KEYSERVER:-subkeys.pgp.net}
367 REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"}
368 USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-"$HOME"/.ssh/authorized_keys}
369 USER_KNOWN_HOSTS=${USER_KNOWN_HOSTS:-"$HOME"/.ssh/known_hosts}
370 HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-}
372 # export USER and GNUPGHOME variables, since they are used by gpg
377 hostKeysCacheDir="$STAGING_AREA"/host_keys
378 userKeysCacheDir="$STAGING_AREA"/user_keys
379 msKnownHosts="$STAGING_AREA"/known_hosts
380 msAuthorizedKeys="$STAGING_AREA"/authorized_keys
382 # make sure gpg home exists with proper permissions
383 mkdir -p -m 0700 "$GNUPGHOME"
386 if [ "$mode" = 'known_hosts' -o "$mode" = 'k' ] ; then
389 cacheDir="$hostKeysCacheDir"
391 log "user '$USER': monkeysphere known_hosts processing"
393 # touch the known_hosts file to make sure it exists
394 touch "$USER_KNOWN_HOSTS"
396 # if hosts are specified on the command line, process just
400 process_host "$host" "$cacheDir"
403 # otherwise, if no hosts are specified, process the user
406 if [ ! -s "$USER_KNOWN_HOSTS" ] ; then
407 failure "known_hosts file '$USER_KNOWN_HOSTS' is empty."
409 process_known_hosts "$cacheDir"
412 ## AUTHORIZED_KEYS MODE
413 elif [ "$mode" = 'authorized_keys' -o "$mode" = 'a' ] ; then
414 mode='authorized_keys'
416 cacheDir="$userKeysCacheDir"
418 # check auth ids file
419 if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then
420 log "authorized_user_ids file is empty or does not exist."
424 log "user '$USER': monkeysphere authorized_keys processing"
426 # if userids are specified on the command line, process just
430 if ! grep -q "$userID" "$AUTHORIZED_USER_IDS" ; then
431 log "userid '$userID' not in authorized_user_ids file."
434 log "processing user id: '$userID'"
435 process_user_id "$userID" "$cacheDir" > /dev/null
438 # otherwise, if no userids are specified, process the entire
439 # authorized_user_ids file
441 process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
444 # write output key file
445 log "writing monkeysphere authorized_keys file... "
446 touch "$msAuthorizedKeys"
447 if [ "$(ls "$cacheDir")" ] ; then
448 log -n "adding gpg keys... "
449 cat "$cacheDir"/* > "$msAuthorizedKeys"
452 log "no gpg keys to add."
454 if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then
455 if [ -s "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then
456 log -n "adding user authorized_keys file... "
457 cat "$USER_CONTROLLED_AUTHORIZED_KEYS" >> "$msAuthorizedKeys"
461 log "monkeysphere authorized_keys file generated:"
462 log "$msAuthorizedKeys"
465 failure "unknown command '$mode'."