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