summaryrefslogtreecommitdiff
path: root/src/common
blob: 471e75a6143aa23138602c1d2ef85d65802d80fb (plain)
  1. # -*-shell-script-*-
  2. # Shared sh 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 userID
  39. userID="$1"
  40. # if CHECK_KEYSERVER variable set, check the keyserver
  41. # for the user ID
  42. if [ "CHECK_KEYSERVER" ] ; then
  43. echo 1,2,3,4,5 | \
  44. gpg --quiet --batch --command-fd 0 --with-colons \
  45. --keyserver "$KEYSERVER" \
  46. --search ="$userID" >/dev/null 2>&1
  47. # otherwise just return true
  48. else
  49. return
  50. fi
  51. }
  52. # check that characters are in a string (in an AND fashion).
  53. # used for checking key capability
  54. # check_capability capability a [b...]
  55. check_capability() {
  56. local capability
  57. local capcheck
  58. capability="$1"
  59. shift 1
  60. for capcheck ; do
  61. if echo "$capability" | grep -q -v "$capcheck" ; then
  62. return 1
  63. fi
  64. done
  65. return 0
  66. }
  67. # get the full fingerprint of a key ID
  68. get_key_fingerprint() {
  69. local keyID
  70. keyID="$1"
  71. gpg --list-key --with-colons --fixed-list-mode \
  72. --with-fingerprint "$keyID" | grep "$keyID" | \
  73. grep '^fpr:' | cut -d: -f10
  74. }
  75. # convert escaped characters from gpg output back into original
  76. # character
  77. # FIXME: undo all escape character translation in with-colons gpg output
  78. unescape() {
  79. echo "$1" | sed 's/\\x3a/:/'
  80. }
  81. # convert key from gpg to ssh known_hosts format
  82. gpg2known_hosts() {
  83. local keyID
  84. local host
  85. keyID="$1"
  86. host=$(echo "$2" | sed -e "s|ssh://||")
  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. # convert key from gpg to ssh authorized_keys format
  98. gpg2authorized_keys() {
  99. local keyID
  100. local userID
  101. keyID="$1"
  102. userID="$2"
  103. gpg --export "$keyID" | \
  104. openpgp2ssh "$keyID" | tr -d '\n'
  105. echo " MonkeySphere${DATE}: ${userID}"
  106. }
  107. # userid and key policy checking
  108. # the following checks policy on the returned keys
  109. # - checks that full key has appropriate valididy (u|f)
  110. # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
  111. # - checks that particular desired user id has appropriate validity
  112. # see /usr/share/doc/gnupg/DETAILS.gz
  113. # expects global variable: "MODE"
  114. process_user_id() {
  115. local userID
  116. local cacheDir
  117. local requiredCapability
  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. userID="$1"
  133. cacheDir="$2"
  134. # set the required key capability based on the mode
  135. if [ "$MODE" = 'known_hosts' ] ; then
  136. requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
  137. elif [ "$MODE" = 'authorized_keys' ] ; then
  138. requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
  139. fi
  140. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  141. # fetch keys from keyserver, return 1 if none found
  142. gpg_fetch_userid "$userID" || return 1
  143. # output gpg info for (exact) userid and store
  144. gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \
  145. ="$userID" 2> /dev/null)
  146. # return 1 if there only "tru" lines are output from gpg
  147. if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then
  148. log " key not found."
  149. return 1
  150. fi
  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. # read the contents of the line
  156. type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1)
  157. validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2)
  158. keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5)
  159. uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10)
  160. capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12)
  161. # process based on record type
  162. case $type in
  163. 'pub') # primary keys
  164. # new key, wipe the slate
  165. keyOK=
  166. pubKeyID=
  167. uidOK=
  168. keyIDs=
  169. pubKeyID="$keyid"
  170. # check primary key validity
  171. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  172. log " unacceptable primary key validity ($validity)."
  173. continue
  174. fi
  175. # check capability is not Disabled...
  176. if check_capability "$capability" 'D' ; then
  177. log " key disabled."
  178. continue
  179. fi
  180. # check overall key capability
  181. # must be Encryption and Authentication
  182. if ! check_capability "$capability" $requiredPubCapability ; then
  183. log " unacceptable primary key capability ($capability)."
  184. continue
  185. fi
  186. # mark if primary key is acceptable
  187. keyOK=true
  188. # add primary key ID to key list if it has required capability
  189. if check_capability "$capability" $requiredCapability ; then
  190. keyIDs[${#keyIDs[*]}]="$keyid"
  191. fi
  192. ;;
  193. 'uid') # user ids
  194. # check key ok and we have key fingerprint
  195. if [ -z "$keyOK" ] ; then
  196. continue
  197. fi
  198. # check key validity
  199. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  200. continue
  201. fi
  202. # check the uid matches
  203. if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
  204. continue
  205. fi
  206. # mark if uid acceptable
  207. uidOK=true
  208. ;;
  209. 'sub') # sub keys
  210. # add sub key ID to key list if it has required capability
  211. if check_capability "$capability" $requiredCapability ; then
  212. keyIDs[${#keyIDs[*]}]="$keyid"
  213. fi
  214. ;;
  215. esac
  216. done
  217. # hash userid for cache file name
  218. userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }')
  219. # make sure the cache directory exists
  220. mkdir -p "$cacheDir"
  221. # touch/clear key cache file
  222. # (will be left empty if there are noacceptable keys)
  223. > "$cacheDir"/"$userIDHash"."$pubKeyID"
  224. # for each acceptable key, write an ssh key line to the
  225. # key cache file
  226. if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then
  227. for keyID in ${keyIDs[@]} ; do
  228. log " acceptable key/uid found."
  229. if [ "$MODE" = 'known_hosts' ] ; then
  230. # export the key
  231. gpg2known_hosts "$keyID" "$userID" >> \
  232. "$cacheDir"/"$userIDHash"."$pubKeyID"
  233. # hash the cache file if specified
  234. if [ "$HASH_KNOWN_HOSTS" ] ; then
  235. ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1
  236. rm "$cacheDir"/"$userIDHash"."$pubKeyID".old
  237. fi
  238. elif [ "$MODE" = 'authorized_keys' ] ; then
  239. # export the key
  240. # FIXME: needs to apply extra options for authorized_keys
  241. # lines if specified
  242. gpg2authorized_keys "$keyID" "$userID" >> \
  243. "$cacheDir"/"$userIDHash"."$pubKeyID"
  244. fi
  245. done
  246. fi
  247. # echo the path to the key cache file
  248. echo "$cacheDir"/"$userIDHash"."$pubKeyID"
  249. }
  250. # update the cache for userid, and prompt to add file to
  251. # authorized_user_ids file if the userid is found in gpg
  252. # and not already in file.
  253. update_userid() {
  254. local userID
  255. local cacheDir
  256. local keyCache
  257. userID="$1"
  258. cacheDir="$2"
  259. log "processing userid: '$userID'"
  260. # return 1 if there is no output of the user ID processing
  261. # ie. no key was found
  262. keyCachePath=$(process_user_id "$userID" "$cacheDir")
  263. if [ -z "$keyCachePath" ] ; then
  264. return 1
  265. fi
  266. # check if user ID is in the authorized_user_ids file
  267. if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
  268. read -p "user ID not currently authorized. authorize? [Y|n]: " OK; OK=${OK:=Y}
  269. if [ ${OK/y/Y} = 'Y' ] ; then
  270. # add if specified
  271. log -n "adding user ID to authorized_user_ids file... "
  272. echo "$userID" >> "$AUTHORIZED_USER_IDS"
  273. echo "done."
  274. else
  275. # else do nothing
  276. log "authorized_user_ids file untouched."
  277. fi
  278. fi
  279. }
  280. # remove a userid from the authorized_user_ids file
  281. remove_userid() {
  282. local userID
  283. userID="$1"
  284. log "processing userid: '$userID'"
  285. if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then
  286. log "user ID not currently authorized."
  287. return 1
  288. fi
  289. log -n "removing user ID '$userID'... "
  290. grep -v "$userID" "$AUTHORIZED_USER_IDS" | sponge "$AUTHORIZED_USER_IDS"
  291. echo "done."
  292. }
  293. # process a host for addition to a known_host file
  294. process_host() {
  295. local host
  296. local cacheDir
  297. local keyCachePath
  298. host="$1"
  299. cacheDir="$2"
  300. log "processing host: $host"
  301. keyCachePath=$(process_user_id "ssh://${host}" "$cacheDir")
  302. if [ $? = 0 ] ; then
  303. ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS"
  304. cat "$keyCachePath" >> "$USER_KNOWN_HOSTS"
  305. fi
  306. }
  307. # process known_hosts file
  308. # go through line-by-line, extract each host, and process with the
  309. # host processing function
  310. process_known_hosts() {
  311. local cacheDir
  312. local hosts
  313. local host
  314. cacheDir="$1"
  315. # take all the hosts from the known_hosts file (first field),
  316. # grep out all the hashed hosts (lines starting with '|')...
  317. meat "$USER_KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | \
  318. while IFS=, read -r -a hosts ; do
  319. # ...and process each host
  320. for host in ${hosts[*]} ; do
  321. process_host "$host" "$cacheDir"
  322. done
  323. done
  324. }
  325. # update an authorized_keys file after first processing the
  326. # authorized_user_ids file
  327. update_authorized_keys() {
  328. local msAuthorizedKeys
  329. local userAuthorizedKeys
  330. local cacheDir
  331. msAuthorizedKeys="$1"
  332. userAuthorizedKeys="$2"
  333. cacheDir="$3"
  334. process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir"
  335. # write output key file
  336. log "writing monkeysphere authorized_keys file... "
  337. touch "$msAuthorizedKeys"
  338. if [ "$(ls "$cacheDir")" ] ; then
  339. log -n "adding gpg keys... "
  340. cat "$cacheDir"/* > "$msAuthorizedKeys"
  341. echo "done."
  342. else
  343. log "no gpg keys to add."
  344. fi
  345. if [ "$userAuthorizedKeys" -a -s "$userAuthorizedKeys" ] ; then
  346. log -n "adding user authorized_keys file... "
  347. cat "$userAuthorizedKeys" >> "$msAuthorizedKeys"
  348. echo "done."
  349. fi
  350. log "monkeysphere authorized_keys file generated:"
  351. log "$msAuthorizedKeys"
  352. }
  353. # process an authorized_*_ids file
  354. # go through line-by-line, extract each userid, and process
  355. process_authorized_ids() {
  356. local authorizedIDs
  357. local cacheDir
  358. local userID
  359. authorizedIDs="$1"
  360. cacheDir="$2"
  361. # clean out keys file and remake keys directory
  362. rm -rf "$cacheDir"
  363. mkdir -p "$cacheDir"
  364. # loop through all user ids in file
  365. # FIXME: needs to handle authorized_keys options
  366. cat "$authorizedIDs" | meat | \
  367. while read -r userID ; do
  368. # process the userid
  369. log "processing userid: '$userID'"
  370. process_user_id "$userID" "$cacheDir" > /dev/null
  371. done
  372. }
  373. # EXPERIMENTAL (unused) process userids found in authorized_keys file
  374. # go through line-by-line, extract monkeysphere userids from comment
  375. # fields, and process each userid
  376. process_authorized_keys() {
  377. local authorizedKeys
  378. local cacheDir
  379. local userID
  380. authorizedKeys="$1"
  381. cacheDir="$2"
  382. # take all the monkeysphere userids from the authorized_keys file
  383. # comment field (third field) that starts with "MonkeySphere uid:"
  384. # FIXME: needs to handle authorized_keys options (field 0)
  385. cat "$authorizedKeys" | \
  386. while read -r options keytype key comment ; do
  387. # if the comment field is empty, assume the third field was
  388. # the comment
  389. if [ -z "$comment" ] ; then
  390. comment="$key"
  391. fi
  392. if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then
  393. continue
  394. fi
  395. userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://")
  396. if [ -z "$userID" ] ; then
  397. continue
  398. fi
  399. # process the userid
  400. log "processing userid: '$userID'"
  401. process_user_id "$userID" "$cacheDir" > /dev/null
  402. done
  403. }
  404. # retrieve key from web of trust, and set owner trust to "full"
  405. # if key is found.
  406. trust_key() {
  407. # get the key from the key server
  408. if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
  409. log "could not retrieve key '$keyID'"
  410. return 1
  411. fi
  412. # get key fingerprint
  413. fingerprint=$(get_key_fingerprint "$keyID")
  414. # attach a "non-exportable" signature to the key
  415. # this is required for the key to have any validity at all
  416. # the 'y's on stdin indicates "yes, i really want to sign"
  417. echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
  418. # import "full" trust for fingerprint into gpg
  419. echo ${fingerprint}:5: | gpg --import-ownertrust
  420. if [ $? = 0 ] ; then
  421. log "owner trust updated."
  422. else
  423. failure "there was a problem changing owner trust."
  424. fi
  425. }
  426. # publish server key to keyserver
  427. publish_server_key() {
  428. read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
  429. if [ ${OK/y/Y} != 'Y' ] ; then
  430. failure "aborting."
  431. fi
  432. # publish host key
  433. # FIXME: need to figure out better way to identify host key
  434. # dummy command so as not to publish fakes keys during testing
  435. # eventually:
  436. #gpg --send-keys --keyserver "$KEYSERVER" $(hostname -f)
  437. echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)"
  438. }