summaryrefslogtreecommitdiff
path: root/rhesus/rhesus
blob: f607f0b9c750ff05deb4867ead5ffee8ce09b013 (plain)
  1. #!/bin/sh
  2. # rhesus: monkeysphere authorized_keys/known_hosts generating script
  3. #
  4. # Written by
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. #
  7. # Copyright 2008, released under the GPL, version 3 or later
  8. # all caps variables are meant to be user supplied (ie. from config
  9. # file) and are considered global
  10. PGRM=$(basename $0)
  11. # date in UTF format if needed
  12. DATE=$(date -u '+%FT%T')
  13. # unset some environment variables that could screw things up
  14. GREP_OPTIONS=
  15. ########################################################################
  16. # FUNCTIONS
  17. ########################################################################
  18. usage() {
  19. cat <<EOF
  20. usage: $PGRM k|known_hosts [host...]
  21. $PGRM a|authorized_keys [userid...]
  22. Monkeysphere update of known_hosts or authorized_keys file.
  23. If hosts/userids are specified, only those specified will be processed
  24. EOF
  25. }
  26. failure() {
  27. echo "$1" >&2
  28. exit ${2:-'1'}
  29. }
  30. # write output to stdout
  31. log() {
  32. echo -n "ms: "
  33. echo "$@"
  34. }
  35. # write output to stderr
  36. loge() {
  37. echo -n "ms: " 1>&2
  38. echo "$@" 1>&2
  39. }
  40. # cut out all comments(#) and blank lines from standard input
  41. meat() {
  42. grep -v -e "^[[:space:]]*#" -e '^$'
  43. }
  44. # cut a specified line from standard input
  45. cutline() {
  46. head --line="$1" | tail -1
  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. # check that characters are in a string (in an AND fashion).
  60. # used for checking key capability
  61. # check_capability capability a [b...]
  62. check_capability() {
  63. local capability
  64. local capcheck
  65. capability="$1"
  66. shift 1
  67. for capcheck ; do
  68. if echo "$capability" | grep -q -v "$capcheck" ; then
  69. return 1
  70. fi
  71. done
  72. return 0
  73. }
  74. # convert escaped characters from gpg output back into original
  75. # character
  76. # FIXME: undo all escape character translation in with-colons gpg output
  77. unescape() {
  78. echo "$1" | sed 's/\\x3a/:/'
  79. }
  80. # stand in until we get dkg's gpg2ssh program
  81. gpg2ssh_tmp() {
  82. local mode
  83. local keyID
  84. local userID
  85. local host
  86. mode="$1"
  87. keyID="$2"
  88. userID="$3"
  89. if [ "$mode" = 'authorized_keys' ] ; then
  90. gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${userID}/"
  91. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  92. # all lines in the known_hosts file. why?
  93. # NOTE: just in case, the COMMENT can be matched with the
  94. # following regexp:
  95. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  96. elif [ "$mode" = 'known_hosts' ] ; then
  97. host=$(echo "$userID" | sed -e "s|ssh://||")
  98. echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/"
  99. fi
  100. }
  101. # userid and key policy checking
  102. # the following checks policy on the returned keys
  103. # - checks that full key has appropriate valididy (u|f)
  104. # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
  105. # - checks that particular desired user id has appropriate validity
  106. # see /usr/share/doc/gnupg/DETAILS.gz
  107. # expects global variable: "mode"
  108. process_user_id() {
  109. local userID
  110. local cacheDir
  111. local requiredPubCapability
  112. local gpgOut
  113. local line
  114. local type
  115. local validity
  116. local keyid
  117. local uidfpr
  118. local capability
  119. local keyOK
  120. local pubKeyID
  121. local uidOK
  122. local keyIDs
  123. local userIDHash
  124. local keyID
  125. userID="$1"
  126. cacheDir="$2"
  127. requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]")
  128. # fetch keys from keyserver, return 1 if none found
  129. gpg_fetch_keys "$userID" || return 1
  130. # output gpg info for (exact) userid and store
  131. gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
  132. ="$userID" 2> /dev/null)
  133. # return 1 if there only "tru" lines are output from gpg
  134. if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
  135. return 1
  136. fi
  137. # loop over all lines in the gpg output and process.
  138. # need to do it this way (as opposed to "while read...") so that
  139. # variables set in loop will be visible outside of loop
  140. for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do
  141. # read the contents of the line
  142. type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1)
  143. validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2)
  144. keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5)
  145. uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10)
  146. capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12)
  147. # process based on record type
  148. case $type in
  149. 'pub') # primary keys
  150. # new key, wipe the slate
  151. keyOK=
  152. pubKeyID=
  153. uidOK=
  154. keyIDs=
  155. pubKeyID="$keyid"
  156. # check primary key validity
  157. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  158. loge " unacceptable primary key validity ($validity)."
  159. continue
  160. fi
  161. # check capability is not Disabled...
  162. if check_capability "$capability" 'D' ; then
  163. loge " key disabled."
  164. continue
  165. fi
  166. # check overall key capability
  167. # must be Encryption and Authentication
  168. if ! check_capability "$capability" $requiredPubCapability ; then
  169. loge " unacceptable primary key capability ($capability)."
  170. continue
  171. fi
  172. # mark if primary key is acceptable
  173. keyOK=true
  174. # add primary key ID to key list if it has required capability
  175. if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
  176. keyIDs[${#keyIDs[*]}]="$keyid"
  177. fi
  178. ;;
  179. 'uid') # user ids
  180. # check key ok and we have key fingerprint
  181. if [ -z "$keyOK" ] ; then
  182. continue
  183. fi
  184. # check key validity
  185. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  186. continue
  187. fi
  188. # check the uid matches
  189. if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
  190. continue
  191. fi
  192. # mark if uid acceptable
  193. uidOK=true
  194. ;;
  195. 'sub') # sub keys
  196. # add sub key ID to key list if it has required capability
  197. if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then
  198. keyIDs[${#keyIDs[*]}]="$keyid"
  199. fi
  200. ;;
  201. esac
  202. done
  203. # hash userid for cache file name
  204. userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }')
  205. # touch/clear key cache file
  206. # (will be left empty if there are noacceptable keys)
  207. > "$cacheDir"/"$userIDHash"."$pubKeyID"
  208. # for each acceptable key, write an ssh key line to the
  209. # key cache file
  210. if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
  211. for keyID in ${keyIDs[@]} ; do
  212. # export the key with gpg2ssh
  213. # FIXME: needs to apply extra options for authorized_keys
  214. # lines if specified
  215. gpg2ssh_tmp "$mode" "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID"
  216. # hash the cache file if specified
  217. if [ "$mode" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then
  218. ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
  219. rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
  220. fi
  221. done
  222. fi
  223. # echo the path to the key cache file
  224. echo "$cacheDir"/"$userIDHash"."$pubKeyID"
  225. }
  226. # process a host for addition to a known_host file
  227. process_host() {
  228. local host
  229. local cacheDir
  230. local hostKeyCachePath
  231. host="$1"
  232. cacheDir="$2"
  233. log "processing host: '$host'"
  234. hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
  235. if [ $? = 0 ] ; then
  236. ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
  237. cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS"
  238. fi
  239. }
  240. # process known_hosts file
  241. # go through line-by-line, extract each host, and process with the
  242. # host processing function
  243. process_known_hosts() {
  244. local cacheDir
  245. local userID
  246. cacheDir="$1"
  247. # take all the hosts from the known_hosts file (first field),
  248. # grep out all the hashed hosts (lines starting with '|')
  249. cut -d ' ' -f 1 "$USER_KNOWN_HOSTS" | \
  250. grep -v '^|.*$' | \
  251. while IFS=, read -r -a hosts ; do
  252. # process each host
  253. for host in ${hosts[*]} ; do
  254. process_host "$host" "$cacheDir"
  255. done
  256. done
  257. }
  258. # process an authorized_*_ids file
  259. # go through line-by-line, extract each userid, and process
  260. process_authorized_ids() {
  261. local authorizedIDsFile
  262. local cacheDir
  263. local userID
  264. local userKeyCachePath
  265. authorizedIDsFile="$1"
  266. cacheDir="$2"
  267. # clean out keys file and remake keys directory
  268. rm -rf "$cacheDir"
  269. mkdir -p "$cacheDir"
  270. # loop through all user ids in file
  271. # FIXME: needs to handle extra options if necessary
  272. cat "$authorizedIDsFile" | meat | \
  273. while read -r userID ; do
  274. # process the userid
  275. log "processing userid: '$userID'"
  276. userKeyCachePath=$(process_user_id "$userID" "$cacheDir")
  277. if [ -s "$userKeyCachePath" ] ; then
  278. loge " acceptable key/uid found."
  279. fi
  280. done
  281. }
  282. ########################################################################
  283. # MAIN
  284. ########################################################################
  285. if [ -z "$1" ] ; then
  286. usage
  287. exit 1
  288. fi
  289. # mode given in first variable
  290. mode="$1"
  291. shift 1
  292. # check user
  293. if ! id -u "$USER" > /dev/null 2>&1 ; then
  294. failure "invalid user '$USER'."
  295. fi
  296. # set user home directory
  297. HOME=$(getent passwd "$USER" | cut -d: -f6)
  298. # set ms home directory
  299. MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere}
  300. # load configuration file
  301. MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf}
  302. [ -e "$MS_CONF" ] && . "$MS_CONF"
  303. # set config variable defaults
  304. STAGING_AREA=${STAGING_AREA:-"$MS_HOME"}
  305. AUTHORIZED_USER_IDS=${AUTHORIZED_USER_IDS:-"$MS_HOME"/authorized_user_ids}
  306. GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg}
  307. KEYSERVER=${KEYSERVER:-subkeys.pgp.net}
  308. REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"}
  309. USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-"$HOME"/.ssh/authorized_keys}
  310. USER_KNOWN_HOSTS=${USER_KNOWN_HOSTS:-"$HOME"/.ssh/known_hosts}
  311. HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-}
  312. # export USER and GNUPGHOME variables, since they are used by gpg
  313. export USER
  314. export GNUPGHOME
  315. # stagging locations
  316. hostKeysCacheDir="$STAGING_AREA"/host_keys
  317. userKeysCacheDir="$STAGING_AREA"/user_keys
  318. msKnownHosts="$STAGING_AREA"/known_hosts
  319. msAuthorizedKeys="$STAGING_AREA"/authorized_keys
  320. # make sure gpg home exists with proper permissions
  321. mkdir -p -m 0700 "$GNUPGHOME"
  322. ## KNOWN_HOST MODE
  323. if [ "$mode" = 'known_hosts' -o "$mode" = 'k' ] ; then
  324. mode='known_hosts'
  325. cacheDir="$hostKeysCacheDir"
  326. log "user '$USER': monkeysphere known_hosts processing"
  327. # touch the known_hosts file to make sure it exists
  328. touch "$USER_KNOWN_HOSTS"
  329. # if hosts are specified on the command line, process just
  330. # those hosts
  331. if [ "$1" ] ; then
  332. for host ; do
  333. process_host "$host" "$cacheDir"
  334. done
  335. # otherwise, if no hosts are specified, process the user
  336. # known_hosts file
  337. else
  338. if [ ! -s "$USER_KNOWN_HOSTS" ] ; then
  339. failure "known_hosts file '$USER_KNOWN_HOSTS' is empty."
  340. fi
  341. process_known_hosts "$cacheDir"
  342. fi
  343. ## AUTHORIZED_KEYS MODE
  344. elif [ "$mode" = 'authorized_keys' -o "$mode" = 'a' ] ; then
  345. mode='authorized_keys'
  346. cacheDir="$userKeysCacheDir"
  347. # check auth ids file
  348. if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then
  349. log "authorized_user_ids file is empty or does not exist."
  350. exit
  351. fi
  352. log "user '$USER': monkeysphere authorized_keys processing"
  353. # if userids are specified on the command line, process just
  354. # those userids
  355. if [ "$1" ] ; then
  356. for userID ; do
  357. if ! grep -q "$userID" "$AUTHORIZED_USER_IDS" ; then
  358. log "userid '$userID' not in authorized_user_ids file."
  359. continue
  360. fi
  361. log "processing user id: '$userID'"
  362. process_user_id "$userID" "$cacheDir" > /dev/null
  363. done
  364. # otherwise, if no userids are specified, process the entire
  365. # authorized_user_ids file
  366. else
  367. process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
  368. fi
  369. # write output key file
  370. log "writing monkeysphere authorized_keys file... "
  371. touch "$msAuthorizedKeys"
  372. if [ "$(ls "$cacheDir")" ] ; then
  373. log -n "adding gpg keys... "
  374. cat "$cacheDir"/* > "$msAuthorizedKeys"
  375. echo "done."
  376. else
  377. log "no gpg keys to add."
  378. fi
  379. if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then
  380. if [ -s "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then
  381. log -n "adding user authorized_keys file... "
  382. cat "$USER_CONTROLLED_AUTHORIZED_KEYS" >> "$msAuthorizedKeys"
  383. echo "done."
  384. fi
  385. fi
  386. log "monkeysphere authorized_keys file generated:"
  387. log "$msAuthorizedKeys"
  388. else
  389. failure "unknown command '$mode'."
  390. fi