summaryrefslogtreecommitdiff
path: root/src/common
blob: 19b5485a37feb2118edb350ae0e34da67e36613a (plain)
  1. # -*-shell-script-*-
  2. # Shared bash functions for the monkeysphere
  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. ########################################################################
  11. # managed directories
  12. ETC="/etc/monkeysphere"
  13. export ETC
  14. CACHE="/var/cache/monkeysphere"
  15. export CACHE
  16. ########################################################################
  17. failure() {
  18. echo "$1" >&2
  19. exit ${2:-'1'}
  20. }
  21. # write output to stderr
  22. log() {
  23. echo -n "ms: " 1>&2
  24. echo "$@" 1>&2
  25. }
  26. # cut out all comments(#) and blank lines from standard input
  27. meat() {
  28. grep -v -e "^[[:space:]]*#" -e '^$'
  29. }
  30. # cut a specified line from standard input
  31. cutline() {
  32. head --line="$1" | tail -1
  33. }
  34. # retrieve all keys with given user id from keyserver
  35. # FIXME: need to figure out how to retrieve all matching keys
  36. # (not just first 5)
  37. gpg_fetch_userid() {
  38. local id
  39. id="$1"
  40. echo 1,2,3,4,5 | \
  41. gpg --quiet --batch --command-fd 0 --with-colons \
  42. --keyserver "$KEYSERVER" \
  43. --search ="$id" >/dev/null 2>&1
  44. }
  45. # check that characters are in a string (in an AND fashion).
  46. # used for checking key capability
  47. # check_capability capability a [b...]
  48. check_capability() {
  49. local capability
  50. local capcheck
  51. capability="$1"
  52. shift 1
  53. for capcheck ; do
  54. if echo "$capability" | grep -q -v "$capcheck" ; then
  55. return 1
  56. fi
  57. done
  58. return 0
  59. }
  60. # get the full fingerprint of a key ID
  61. get_key_fingerprint() {
  62. local keyID
  63. keyID="$1"
  64. gpg --list-key --with-colons --fixed-list-mode \
  65. --with-fingerprint "$keyID" | grep "$keyID" | \
  66. grep '^fpr:' | cut -d: -f10
  67. }
  68. # convert escaped characters from gpg output back into original
  69. # character
  70. # FIXME: undo all escape character translation in with-colons gpg output
  71. unescape() {
  72. echo "$1" | sed 's/\\x3a/:/'
  73. }
  74. # convert key from gpg to ssh known_hosts format
  75. gpg2known_hosts() {
  76. local keyID
  77. local host
  78. keyID="$1"
  79. host=$(echo "$2" | sed -e "s|ssh://||")
  80. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  81. # all lines in the known_hosts file. why?
  82. # NOTE: just in case, the COMMENT can be matched with the
  83. # following regexp:
  84. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  85. echo -n "$host "
  86. gpg --export "$keyID" | \
  87. openpgp2ssh "$keyID" | tr -d '\n'
  88. echo " MonkeySphere${DATE}"
  89. }
  90. # convert key from gpg to ssh authorized_keys format
  91. gpg2authorized_keys() {
  92. local keyID
  93. local userID
  94. keyID="$1"
  95. userID="$2"
  96. gpg --export "$keyID" | \
  97. openpgp2ssh "$keyID" | tr -d '\n'
  98. echo " MonkeySphere${DATE}:${userID}"
  99. }
  100. # userid and key policy checking
  101. # the following checks policy on the returned keys
  102. # - checks that full key has appropriate valididy (u|f)
  103. # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
  104. # - checks that particular desired user id has appropriate validity
  105. # see /usr/share/doc/gnupg/DETAILS.gz
  106. # expects global variable: "MODE"
  107. process_user_id() {
  108. local userID
  109. local cacheDir
  110. local requiredPubCapability
  111. local gpgOut
  112. local line
  113. local type
  114. local validity
  115. local keyid
  116. local uidfpr
  117. local capability
  118. local keyOK
  119. local pubKeyID
  120. local uidOK
  121. local keyIDs
  122. local userIDHash
  123. local keyID
  124. userID="$1"
  125. cacheDir="$2"
  126. requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]")
  127. # fetch keys from keyserver, return 1 if none found
  128. gpg_fetch_userid "$userID" || return 1
  129. # output gpg info for (exact) userid and store
  130. gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
  131. ="$userID" 2> /dev/null)
  132. # return 1 if there only "tru" lines are output from gpg
  133. if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
  134. log " key not found."
  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. log " unacceptable primary key validity ($validity)."
  159. continue
  160. fi
  161. # check capability is not Disabled...
  162. if check_capability "$capability" 'D' ; then
  163. log " 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. log " 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. # make sure the cache directory exists
  206. mkdir -p "$cacheDir"
  207. # touch/clear key cache file
  208. # (will be left empty if there are noacceptable keys)
  209. > "$cacheDir"/"$userIDHash"."$pubKeyID"
  210. # for each acceptable key, write an ssh key line to the
  211. # key cache file
  212. if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
  213. for keyID in ${keyIDs[@]} ; do
  214. log " acceptable key/uid found."
  215. if [ "$MODE" = 'known_hosts' ] ; then
  216. # export the key
  217. gpg2known_hosts "$keyID" "$userID" >> \
  218. "$cacheDir"/"$userIDHash"."$pubKeyID"
  219. # hash the cache file if specified
  220. if [ "$HASH_KNOWN_HOSTS" ] ; then
  221. ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
  222. rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
  223. fi
  224. elif [ "$MODE" = 'authorized_keys' ] ; then
  225. # export the key
  226. # FIXME: needs to apply extra options for authorized_keys
  227. # lines if specified
  228. gpg2authorized_keys "$keyID" "$userID" >> \
  229. "$cacheDir"/"$userIDHash"."$pubKeyID"
  230. fi
  231. done
  232. fi
  233. # echo the path to the key cache file
  234. echo "$cacheDir"/"$userIDHash"."$pubKeyID"
  235. }
  236. # update the cache for userid, and prompt to add file to
  237. # authorized_user_ids file if the userid is found in gpg
  238. # and not already in file.
  239. update_userid() {
  240. local userID
  241. local cacheDir
  242. local userIDKeyCache
  243. userID="$1"
  244. cacheDir="$2"
  245. log "processing userid: '$userID'"
  246. userIDKeyCache=$(process_user_id "$userID" "$cacheDir")
  247. if [ -z "$userIDKeyCache" ] ; then
  248. return 1
  249. fi
  250. if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
  251. read -p "user ID not currently authorized. authorize? [Y|n]: " OK; OK=${OK:=Y}
  252. if [ ${OK/y/Y} = 'Y' ] ; then
  253. log -n "adding user ID to authorized_user_ids file... "
  254. echo "$userID" >> "$AUTHORIZED_USER_IDS"
  255. echo "done."
  256. else
  257. log "authorized_user_ids file untouched."
  258. fi
  259. fi
  260. }
  261. # remove a userid from the authorized_user_ids file
  262. remove_userid() {
  263. local userID
  264. userID="$1"
  265. log "processing userid: '$userID'"
  266. if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
  267. log "user ID not currently authorized."
  268. return 1
  269. fi
  270. log -n "removing user ID '$userID'... "
  271. grep -v "$userID" "$AUTHORIZED_USER_IDS" | sponge "$AUTHORIZED_USER_IDS"
  272. echo "done."
  273. }
  274. # process a host for addition to a known_host file
  275. process_host() {
  276. local host
  277. local cacheDir
  278. local hostKeyCachePath
  279. host="$1"
  280. cacheDir="$2"
  281. log "processing host: '$host'"
  282. hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
  283. if [ $? = 0 ] ; then
  284. ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
  285. cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS"
  286. fi
  287. }
  288. # process known_hosts file
  289. # go through line-by-line, extract each host, and process with the
  290. # host processing function
  291. process_known_hosts() {
  292. local knownHosts
  293. local cacheDir
  294. local hosts
  295. local host
  296. knownHosts="$1"
  297. cacheDir="$2"
  298. # take all the hosts from the known_hosts file (first field),
  299. # grep out all the hashed hosts (lines starting with '|')...
  300. cut -d ' ' -f 1 "$knownHosts" | \
  301. grep -v '^|.*$' | \
  302. while IFS=, read -r -a hosts ; do
  303. # ...and process each host
  304. for host in ${hosts[*]} ; do
  305. process_host "$host" "$cacheDir"
  306. done
  307. done
  308. }
  309. # update an authorized_keys file after first processing the
  310. # authorized_user_ids file
  311. update_authorized_keys() {
  312. local msAuthorizedKeys
  313. local userAuthorizedKeys
  314. local cacheDir
  315. msAuthorizedKeys="$1"
  316. userAuthorizedKeys="$2"
  317. cacheDir="$3"
  318. process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
  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:"
  335. log "$msAuthorizedKeys"
  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. authorizedIDs="$1"
  344. cacheDir="$2"
  345. # clean out keys file and remake keys directory
  346. rm -rf "$cacheDir"
  347. mkdir -p "$cacheDir"
  348. # loop through all user ids in file
  349. # FIXME: needs to handle authorized_keys options
  350. cat "$authorizedIDs" | meat | \
  351. while read -r userID ; do
  352. # process the userid
  353. log "processing userid: '$userID'"
  354. process_user_id "$userID" "$cacheDir" > /dev/null
  355. done
  356. }
  357. # EXPERIMENTAL (unused) process userids found in authorized_keys file
  358. # go through line-by-line, extract monkeysphere userids from comment
  359. # fields, and process each userid
  360. process_userids_from_authorized_keys() {
  361. local authorizedKeys
  362. local cacheDir
  363. local userID
  364. authorizedKeys="$1"
  365. cacheDir="$2"
  366. # take all the monkeysphere userids from the authorized_keys file
  367. # comment field (third field) that starts with "MonkeySphere uid:"
  368. # FIXME: needs to handle authorized_keys options (field 0)
  369. cat "$authorizedKeys" | \
  370. while read -r options keytype key comment ; do
  371. # if the comment field is empty, assume the third field was
  372. # the comment
  373. if [ -z "$comment" ] ; then
  374. comment="$key"
  375. fi
  376. if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
  377. continue
  378. fi
  379. userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
  380. if [ -z "$userID" ] ; then
  381. continue
  382. fi
  383. # process the userid
  384. log "processing userid: '$userID'"
  385. process_user_id "$userID" "$cacheDir" > /dev/null
  386. done
  387. }
  388. # retrieve key from web of trust, and set owner trust to "full"
  389. # if key is found.
  390. trust_key() {
  391. # get the key from the key server
  392. if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
  393. log "could not retrieve key '$keyID'"
  394. return 1
  395. fi
  396. # get key fingerprint
  397. fingerprint=$(get_key_fingerprint "$keyID")
  398. # attach a "non-exportable" signature to the key
  399. # this is required for the key to have any validity at all
  400. # the 'y's on stdin indicates "yes, i really want to sign"
  401. echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
  402. # import "full" trust for fingerprint into gpg
  403. echo ${fingerprint}:5: | gpg --import-ownertrust
  404. if [ $? = 0 ] ; then
  405. log "owner trust updated."
  406. else
  407. failure "there was a problem changing owner trust."
  408. fi
  409. }
  410. # publish server key to keyserver
  411. publish_server_key() {
  412. read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
  413. if [ ${OK/y/Y} != 'Y' ] ; then
  414. failure "aborting."
  415. fi
  416. # publish host key
  417. # FIXME: need to figure out better way to identify host key
  418. # dummy command so as not to publish fakes keys during testing
  419. # eventually:
  420. #gpg --send-keys --keyserver "$KEYSERVER" $(hostname -f)
  421. echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)"
  422. }