summaryrefslogtreecommitdiff
path: root/src/common
blob: 89efc46a4014dbcfedaec546018c09d066e3509b (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. ### COMMON VARIABLES
  12. # managed directories
  13. ETC="/etc/monkeysphere"
  14. export ETC
  15. CACHE="/var/cache/monkeysphere"
  16. export CACHE
  17. ########################################################################
  18. ### UTILITY FUNCTIONS
  19. failure() {
  20. echo "$1" >&2
  21. exit ${2:-'1'}
  22. }
  23. # write output to stderr
  24. log() {
  25. echo -n "ms: " 1>&2
  26. echo "$@" 1>&2
  27. }
  28. loge() {
  29. echo "$@" 1>&2
  30. }
  31. # cut out all comments(#) and blank lines from standard input
  32. meat() {
  33. grep -v -e "^[[:space:]]*#" -e '^$'
  34. }
  35. # cut a specified line from standard input
  36. cutline() {
  37. head --line="$1" | tail -1
  38. }
  39. # check that characters are in a string (in an AND fashion).
  40. # used for checking key capability
  41. # check_capability capability a [b...]
  42. check_capability() {
  43. local usage
  44. local capcheck
  45. usage="$1"
  46. shift 1
  47. for capcheck ; do
  48. if echo "$usage" | grep -q -v "$capcheck" ; then
  49. return 1
  50. fi
  51. done
  52. return 0
  53. }
  54. # convert escaped characters from gpg output back into original
  55. # character
  56. # FIXME: undo all escape character translation in with-colons gpg output
  57. unescape() {
  58. echo "$1" | sed 's/\\x3a/:/'
  59. }
  60. # remove all lines with specified string from specified file
  61. remove_line() {
  62. local file
  63. local string
  64. file="$1"
  65. string="$2"
  66. if [ "$file" -a "$string" ] ; then
  67. grep -v "$string" "$file" | sponge "$file"
  68. fi
  69. }
  70. # translate ssh-style path variables %h and %u
  71. translate_ssh_variables() {
  72. local uname
  73. local home
  74. uname="$1"
  75. path="$2"
  76. # get the user's home directory
  77. userHome=$(getent passwd "$uname" | cut -d: -f6)
  78. # translate ssh-style path variables
  79. path=${path/\%u/"$uname"}
  80. path=${path/\%h/"$userHome"}
  81. echo "$path"
  82. }
  83. ### CONVERTION UTILITIES
  84. # output the ssh key for a given key ID
  85. gpg2ssh() {
  86. local keyID
  87. #keyID="$1" #TMP
  88. # only use last 16 characters until openpgp2ssh can take all 40 #TMP
  89. keyID=$(echo "$1" | cut -c 25-) #TMP
  90. gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
  91. }
  92. # output known_hosts line from ssh key
  93. ssh2known_hosts() {
  94. local host
  95. local key
  96. host="$1"
  97. key="$2"
  98. echo -n "$host "
  99. echo -n "$key" | tr -d '\n'
  100. echo " MonkeySphere${DATE}"
  101. }
  102. # output authorized_keys line from ssh key
  103. ssh2authorized_keys() {
  104. local userID
  105. local key
  106. userID="$1"
  107. key="$2"
  108. echo -n "$key" | tr -d '\n'
  109. echo " MonkeySphere${DATE} ${userID}"
  110. }
  111. # convert key from gpg to ssh known_hosts format
  112. gpg2known_hosts() {
  113. local host
  114. local keyID
  115. host="$1"
  116. keyID="$2"
  117. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  118. # all lines in the known_hosts file. why?
  119. # NOTE: just in case, the COMMENT can be matched with the
  120. # following regexp:
  121. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  122. echo -n "$host "
  123. gpg2ssh "$keyID" | tr -d '\n'
  124. echo " MonkeySphere${DATE}"
  125. }
  126. # convert key from gpg to ssh authorized_keys format
  127. gpg2authorized_keys() {
  128. local userID
  129. local keyID
  130. userID="$1"
  131. keyID="$2"
  132. # NOTE: just in case, the COMMENT can be matched with the
  133. # following regexp:
  134. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  135. gpg2ssh "$keyID" | tr -d '\n'
  136. echo " MonkeySphere${DATE} ${userID}"
  137. }
  138. ### GPG UTILITIES
  139. # retrieve all keys with given user id from keyserver
  140. # FIXME: need to figure out how to retrieve all matching keys
  141. # (not just first N (5 in this case))
  142. gpg_fetch_userid() {
  143. local userID
  144. userID="$1"
  145. log -n " checking keyserver $KEYSERVER... "
  146. echo 1,2,3,4,5 | \
  147. gpg --quiet --batch --with-colons \
  148. --command-fd 0 --keyserver "$KEYSERVER" \
  149. --search ="$userID" > /dev/null 2>&1
  150. loge "done."
  151. }
  152. # get the full fingerprint of a key ID
  153. get_key_fingerprint() {
  154. local keyID
  155. keyID="$1"
  156. gpg --list-key --with-colons --fixed-list-mode \
  157. --with-fingerprint "$keyID" | grep "$keyID" | \
  158. grep '^fpr:' | cut -d: -f10
  159. }
  160. ########################################################################
  161. ### PROCESSING FUNCTIONS
  162. # userid and key policy checking
  163. # the following checks policy on the returned keys
  164. # - checks that full key has appropriate valididy (u|f)
  165. # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
  166. # - checks that requested user ID has appropriate validity
  167. # (see /usr/share/doc/gnupg/DETAILS.gz)
  168. # output is one line for every found key, in the following format:
  169. #
  170. # flag fingerprint
  171. #
  172. # "flag" is an acceptability flag, 0 = ok, 1 = bad
  173. # "fingerprint" is the fingerprint of the key
  174. #
  175. # expects global variable: "MODE"
  176. process_user_id() {
  177. local userID
  178. local requiredCapability
  179. local requiredPubCapability
  180. local gpgOut
  181. local type
  182. local validity
  183. local keyid
  184. local uidfpr
  185. local usage
  186. local keyOK
  187. local uidOK
  188. local lastKey
  189. local lastKeyOK
  190. local fingerprint
  191. userID="$1"
  192. # set the required key capability based on the mode
  193. if [ "$MODE" = 'known_hosts' ] ; then
  194. requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
  195. elif [ "$MODE" = 'authorized_keys' ] ; then
  196. requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
  197. fi
  198. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  199. # if CHECK_KEYSERVER variable set, check the keyserver
  200. # for the user ID
  201. if [ "$CHECK_KEYSERVER" = "true" ] ; then
  202. gpg_fetch_userid "$userID"
  203. fi
  204. # output gpg info for (exact) userid and store
  205. gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
  206. --with-fingerprint --with-fingerprint \
  207. ="$userID" 2>/dev/null)
  208. # if the gpg query return code is not 0, return 1
  209. if [ "$?" -ne 0 ] ; then
  210. log " - key not found."
  211. return 1
  212. fi
  213. # loop over all lines in the gpg output and process.
  214. # need to do it this way (as opposed to "while read...") so that
  215. # variables set in loop will be visible outside of loop
  216. echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
  217. while IFS=: read -r type validity keyid uidfpr usage ; do
  218. # process based on record type
  219. case $type in
  220. 'pub') # primary keys
  221. # new key, wipe the slate
  222. keyOK=
  223. uidOK=
  224. lastKey=pub
  225. lastKeyOK=
  226. fingerprint=
  227. log " primary key found: $keyid"
  228. # if overall key is not valid, skip
  229. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  230. log " - unacceptable primary key validity ($validity)."
  231. continue
  232. fi
  233. # if overall key is disabled, skip
  234. if check_capability "$usage" 'D' ; then
  235. log " - key disabled."
  236. continue
  237. fi
  238. # if overall key capability is not ok, skip
  239. if ! check_capability "$usage" $requiredPubCapability ; then
  240. log " - unacceptable primary key capability ($usage)."
  241. continue
  242. fi
  243. # mark overall key as ok
  244. keyOK=true
  245. # mark primary key as ok if capability is ok
  246. if check_capability "$usage" $requiredCapability ; then
  247. lastKeyOK=true
  248. fi
  249. ;;
  250. 'uid') # user ids
  251. # if an acceptable user ID was already found, skip
  252. if [ "$uidOK" ] ; then
  253. continue
  254. fi
  255. # if the user ID does not match, skip
  256. if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
  257. continue
  258. fi
  259. # if the user ID validity is not ok, skip
  260. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  261. continue
  262. fi
  263. # mark user ID acceptable
  264. uidOK=true
  265. # output a line for the primary key
  266. # 0 = ok, 1 = bad
  267. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  268. log " * acceptable key found."
  269. echo 0 "$fingerprint"
  270. else
  271. echo 1 "$fingerprint"
  272. fi
  273. ;;
  274. 'sub') # sub keys
  275. # unset acceptability of last key
  276. lastKey=sub
  277. lastKeyOK=
  278. fingerprint=
  279. # if sub key validity is not ok, skip
  280. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  281. continue
  282. fi
  283. # if sub key capability is not ok, skip
  284. if ! check_capability "$usage" $requiredCapability ; then
  285. continue
  286. fi
  287. # mark sub key as ok
  288. lastKeyOK=true
  289. ;;
  290. 'fpr') # key fingerprint
  291. fingerprint="$uidfpr"
  292. # if the last key was the pub key, skip
  293. if [ "$lastKey" = pub ] ; then
  294. continue
  295. fi
  296. # output a line for the last subkey
  297. # 0 = ok, 1 = bad
  298. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  299. log " * acceptable key found."
  300. echo 0 "$fingerprint"
  301. else
  302. echo 1 "$fingerprint"
  303. fi
  304. ;;
  305. esac
  306. done
  307. }
  308. # update the cache for userid, and prompt to add file to
  309. # authorized_user_ids file if the userid is found in gpg
  310. # and not already in file.
  311. update_userid() {
  312. local userID
  313. userID="$1"
  314. authorizedUserIDs="$2"
  315. log "processing userid: '$userID'"
  316. # process the user ID to pull it from keyserver
  317. process_user_id "$userID" | grep -q "^0 "
  318. # check if user ID is in the authorized_user_ids file
  319. if ! grep -q "^${userID}\$" "$authorizedUserIDs" ; then
  320. read -p "user ID not currently authorized. authorize? [Y|n]: " OK; OK=${OK:=Y}
  321. if [ ${OK/y/Y} = 'Y' ] ; then
  322. # add if specified
  323. log -n " adding user ID to authorized_user_ids file... "
  324. echo "$userID" >> "$authorizedUserIDs"
  325. loge "done."
  326. else
  327. # else do nothing
  328. log " authorized_user_ids file untouched."
  329. fi
  330. fi
  331. }
  332. # remove a userid from the authorized_user_ids file
  333. remove_userid() {
  334. local userID
  335. userID="$1"
  336. authorizedUserIDs="$2"
  337. log "processing userid: '$userID'"
  338. # check if user ID is in the authorized_user_ids file
  339. if ! grep -q "^${userID}\$" "$authorizedUserIDs" ; then
  340. log " user ID not currently authorized."
  341. return 1
  342. fi
  343. # remove user ID from file
  344. log -n " removing user ID '$userID'... "
  345. remove_line "$authorizedUserIDs" "^${userID}$"
  346. loge "done."
  347. }
  348. # process a host in known_host file
  349. process_host_known_hosts() {
  350. local host
  351. local userID
  352. local ok
  353. local keyid
  354. local tmpfile
  355. host="$1"
  356. userID="ssh://${host}"
  357. log "processing host: $host"
  358. process_user_id "ssh://${host}" | \
  359. while read -r ok keyid ; do
  360. sshKey=$(gpg2ssh "$keyid")
  361. # remove the old host key line
  362. remove_line "$KNOWN_HOSTS" "$sshKey"
  363. # if key OK, add new host line
  364. if [ "$ok" -eq '0' ] ; then
  365. # hash if specified
  366. if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
  367. # FIXME: this is really hackish cause ssh-keygen won't
  368. # hash from stdin to stdout
  369. tmpfile=$(mktemp)
  370. ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
  371. ssh-keygen -H -f "$tmpfile" 2> /dev/null
  372. cat "$tmpfile" >> "$KNOWN_HOSTS"
  373. rm -f "$tmpfile" "${tmpfile}.old"
  374. else
  375. ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
  376. fi
  377. fi
  378. done
  379. }
  380. # process a uid in an authorized_keys file
  381. process_uid_authorized_keys() {
  382. local userID
  383. local ok
  384. local keyid
  385. userID="$1"
  386. log "processing user ID: $userID"
  387. process_user_id "$userID" | \
  388. while read -r ok keyid ; do
  389. sshKey=$(gpg2ssh "$keyid")
  390. # remove the old host key line
  391. remove_line "$AUTHORIZED_KEYS" "$sshKey"
  392. # if key OK, add new host line
  393. if [ "$ok" -eq '0' ] ; then
  394. ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
  395. fi
  396. done
  397. }
  398. # process known_hosts file
  399. # go through line-by-line, extract each host, and process with the
  400. # host processing function
  401. process_known_hosts() {
  402. local hosts
  403. local host
  404. # take all the hosts from the known_hosts file (first field),
  405. # grep out all the hashed hosts (lines starting with '|')...
  406. cat "$KNOWN_HOSTS" | meat | \
  407. cut -d ' ' -f 1 | grep -v '^|.*$' | \
  408. while IFS=, read -r -a hosts ; do
  409. # and process each host
  410. for host in ${hosts[*]} ; do
  411. process_host_known_hosts "$host"
  412. done
  413. done
  414. }
  415. # process an authorized_user_ids file for authorized_keys
  416. process_authorized_user_ids() {
  417. local userid
  418. authorizedUserIDs="$1"
  419. cat "$authorizedUserIDs" | meat | \
  420. while read -r userid ; do
  421. process_uid_authorized_keys "$userid"
  422. done
  423. }
  424. # EXPERIMENTAL (unused) process userids found in authorized_keys file
  425. # go through line-by-line, extract monkeysphere userids from comment
  426. # fields, and process each userid
  427. # NOT WORKING
  428. process_authorized_keys() {
  429. local authorizedKeys
  430. local userID
  431. authorizedKeys="$1"
  432. # take all the monkeysphere userids from the authorized_keys file
  433. # comment field (third field) that starts with "MonkeySphere uid:"
  434. # FIXME: needs to handle authorized_keys options (field 0)
  435. cat "$authorizedKeys" | meat | \
  436. while read -r options keytype key comment ; do
  437. # if the comment field is empty, assume the third field was
  438. # the comment
  439. if [ -z "$comment" ] ; then
  440. comment="$key"
  441. fi
  442. if echo "$comment" | egrep -v -q '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}' ; then
  443. continue
  444. fi
  445. userID=$(echo "$comment" | awk "{ print $2 }")
  446. if [ -z "$userID" ] ; then
  447. continue
  448. fi
  449. # process the userid
  450. log "processing userid: '$userID'"
  451. process_user_id "$userID" > /dev/null
  452. done
  453. }
  454. ##################################################
  455. ### GPG HELPER FUNCTIONS
  456. # retrieve key from web of trust, and set owner trust to "full"
  457. # if key is found.
  458. trust_key() {
  459. # get the key from the key server
  460. if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
  461. log "could not retrieve key '$keyID'"
  462. return 1
  463. fi
  464. # get key fingerprint
  465. fingerprint=$(get_key_fingerprint "$keyID")
  466. # attach a "non-exportable" signature to the key
  467. # this is required for the key to have any validity at all
  468. # the 'y's on stdin indicates "yes, i really want to sign"
  469. echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
  470. # import "full" trust for fingerprint into gpg
  471. echo ${fingerprint}:5: | gpg --import-ownertrust
  472. if [ $? = 0 ] ; then
  473. log "owner trust updated."
  474. else
  475. failure "there was a problem changing owner trust."
  476. fi
  477. }
  478. # publish server key to keyserver
  479. publish_server_key() {
  480. read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
  481. if [ ${OK/y/Y} != 'Y' ] ; then
  482. failure "aborting."
  483. fi
  484. # publish host key
  485. # FIXME: need to figure out better way to identify host key
  486. # dummy command so as not to publish fakes keys during testing
  487. # eventually:
  488. #gpg --keyserver "$KEYSERVER" --send-keys $(hostname -f)
  489. echo "NOT PUBLISHED (to avoid permanent publication errors during monkeysphere development).
  490. To publish manually, do: gpg --keyserver $KEYSERVER --send-keys $(hostname -f)"
  491. return 1
  492. }