summaryrefslogtreecommitdiff
path: root/src/common
blob: 40ba8de0c9eb7dc3504ba3535b737329cd878930 (plain)
  1. # -*-shell-script-*-
  2. # Shared sh functions for the monkeysphere
  3. #
  4. # Written by
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. # Jamie McClelland <jm@mayfirst.org>
  7. # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
  8. #
  9. # Copyright 2008, released under the GPL, version 3 or later
  10. # all-caps variables are meant to be user supplied (ie. from config
  11. # file) and are considered global
  12. ########################################################################
  13. ### COMMON VARIABLES
  14. # managed directories
  15. ETC="/etc/monkeysphere"
  16. export ETC
  17. ########################################################################
  18. ### UTILITY FUNCTIONS
  19. # failure function. exits with code 255, unless specified otherwise.
  20. failure() {
  21. echo "$1" >&2
  22. exit ${2:-'255'}
  23. }
  24. # write output to stderr based on specified LOG_LEVEL the first
  25. # parameter is the priority of the output, and everything else is what
  26. # is echoed to stderr
  27. log() {
  28. local priority
  29. local level
  30. local output
  31. local alllevels
  32. local found=
  33. # don't include SILENT in alllevels: it's handled separately
  34. # list in decreasing verbosity (all caps).
  35. # separate with $IFS explicitly, since we do some fancy footwork
  36. # elsewhere.
  37. alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
  38. # translate lowers to uppers in global log level
  39. LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
  40. # just go ahead and return if the log level is silent
  41. if [ "$LOG_LEVEL" = 'SILENT' ] ; then
  42. return
  43. fi
  44. for level in $alllevels ; do
  45. if [ "$LOG_LEVEL" = "$level" ] ; then
  46. found=true
  47. fi
  48. done
  49. if [ -z "$found" ] ; then
  50. # default to INFO:
  51. LOG_LEVEL=INFO
  52. fi
  53. # get priority from first parameter, translating all lower to
  54. # uppers
  55. priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
  56. shift
  57. # scan over available levels
  58. for level in $alllevels ; do
  59. # output if the log level matches, set output to true
  60. # this will output for all subsequent loops as well.
  61. if [ "$LOG_LEVEL" = "$level" ] ; then
  62. output=true
  63. fi
  64. if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
  65. echo -n "ms: " >&2
  66. echo "$@" >&2
  67. fi
  68. done
  69. }
  70. # cut out all comments(#) and blank lines from standard input
  71. meat() {
  72. grep -v -e "^[[:space:]]*#" -e '^$' "$1"
  73. }
  74. # cut a specified line from standard input
  75. cutline() {
  76. head --line="$1" "$2" | tail -1
  77. }
  78. # this is a wrapper for doing lock functions.
  79. #
  80. # it lets us depend on either lockfile-progs (preferred) or procmail's
  81. # lockfile, and should
  82. lock() {
  83. local use_lockfileprogs=true
  84. local action="$1"
  85. local file="$file"
  86. if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
  87. if ! ( which lockfile >/dev/null ); then
  88. failure "Neither lockfile-create nor lockfile are in the path!"
  89. fi
  90. use_lockfileprogs=
  91. fi
  92. case "$action" in
  93. create)
  94. if [ -n "$use_lockfileprogs" ] ; then
  95. lockfile-create "$file" || failure "unable to lock '$file'"
  96. else
  97. lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
  98. fi
  99. ;;
  100. touch)
  101. if [ -n "$use_lockfileprogs" ] ; then
  102. lockfile-touch --oneshot "$file"
  103. else
  104. : Nothing to do here
  105. fi
  106. ;;
  107. remove)
  108. if [ -n "$use_lockfileprogs" ] ; then
  109. lockfile-remove "$file"
  110. else
  111. rm -f "${file}.lock"
  112. fi
  113. ;;
  114. *)
  115. failure "bad argument for lock subfunction '$action'"
  116. esac
  117. # check that characters are in a string (in an AND fashion).
  118. # used for checking key capability
  119. # check_capability capability a [b...]
  120. check_capability() {
  121. local usage
  122. local capcheck
  123. usage="$1"
  124. shift 1
  125. for capcheck ; do
  126. if echo "$usage" | grep -q -v "$capcheck" ; then
  127. return 1
  128. fi
  129. done
  130. return 0
  131. }
  132. # hash of a file
  133. file_hash() {
  134. md5sum "$1" 2> /dev/null
  135. }
  136. # convert escaped characters in pipeline from gpg output back into
  137. # original character
  138. # FIXME: undo all escape character translation in with-colons gpg
  139. # output
  140. gpg_unescape() {
  141. sed 's/\\x3a/:/g'
  142. }
  143. # convert nasty chars into gpg-friendly form in pipeline
  144. # FIXME: escape everything, not just colons!
  145. gpg_escape() {
  146. sed 's/:/\\x3a/g'
  147. }
  148. # prompt for GPG-formatted expiration, and emit result on stdout
  149. get_gpg_expiration() {
  150. local keyExpire
  151. keyExpire="$1"
  152. if [ -z "$keyExpire" ]; then
  153. cat >&2 <<EOF
  154. Please specify how long the key should be valid.
  155. 0 = key does not expire
  156. <n> = key expires in n days
  157. <n>w = key expires in n weeks
  158. <n>m = key expires in n months
  159. <n>y = key expires in n years
  160. EOF
  161. while [ -z "$keyExpire" ] ; do
  162. read -p "Key is valid for? (0) " keyExpire
  163. if ! test_gpg_expire ${keyExpire:=0} ; then
  164. echo "invalid value" >&2
  165. unset keyExpire
  166. fi
  167. done
  168. elif ! test_gpg_expire "$keyExpire" ; then
  169. failure "invalid key expiration value '$keyExpire'."
  170. fi
  171. echo "$keyExpire"
  172. }
  173. passphrase_prompt() {
  174. local prompt="$1"
  175. local fifo="$2"
  176. local PASS
  177. if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
  178. "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
  179. else
  180. read -s -p "$prompt" PASS
  181. # Uses the builtin echo, so should not put the passphrase into
  182. # the process table. I think. --dkg
  183. echo "$PASS" > "$fifo"
  184. fi
  185. }
  186. test_gnu_dummy_s2k_extension() {
  187. # this block contains a demonstration private key that has had the
  188. # primary key stripped out using the GNU S2K extension known as
  189. # "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz). The subkey is
  190. # present in cleartext, however.
  191. # openpgp2ssh will be able to deal with this based on whether the
  192. # local copy of GnuTLS contains read_s2k support that can handle it.
  193. # read up on that here:
  194. # http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
  195. echo "
  196. -----BEGIN PGP PRIVATE KEY BLOCK-----
  197. Version: GnuPG v1.4.9 (GNU/Linux)
  198. lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
  199. xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
  200. G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
  201. /gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
  202. biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
  203. AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
  204. ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
  205. aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
  206. g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
  207. mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
  208. 8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
  209. +bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
  210. RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
  211. 4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
  212. 7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
  213. ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
  214. kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
  215. Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
  216. SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
  217. vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
  218. vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
  219. vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
  220. =CR5w
  221. -----END PGP PRIVATE KEY BLOCK-----
  222. " | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
  223. }
  224. # remove all lines with specified string from specified file
  225. remove_line() {
  226. local file
  227. local string
  228. local tempfile
  229. file="$1"
  230. string="$2"
  231. if [ -z "$file" -o -z "$string" ] ; then
  232. return 1
  233. fi
  234. if [ ! -e "$file" ] ; then
  235. return 1
  236. fi
  237. # if the string is in the file...
  238. if grep -q -F "$string" "$file" 2> /dev/null ; then
  239. tempfile=$(mktemp "${file}.XXXXXXX") || \
  240. failure "Unable to make temp file '${file}.XXXXXXX'"
  241. # remove the line with the string, and return 0
  242. grep -v -F "$string" "$file" >"$tempfile"
  243. cat "$tempfile" > "$file"
  244. rm "$tempfile"
  245. return 0
  246. # otherwise return 1
  247. else
  248. return 1
  249. fi
  250. }
  251. # remove all lines with MonkeySphere strings in file
  252. remove_monkeysphere_lines() {
  253. local file
  254. local tempfile
  255. file="$1"
  256. if [ -z "$file" ] ; then
  257. return 1
  258. fi
  259. if [ ! -e "$file" ] ; then
  260. return 1
  261. fi
  262. tempfile=$(mktemp "${file}.XXXXXXX") || \
  263. failure "Could not make temporary file '${file}.XXXXXXX'."
  264. egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
  265. "$file" >"$tempfile"
  266. cat "$tempfile" > "$file"
  267. rm "$tempfile"
  268. }
  269. # translate ssh-style path variables %h and %u
  270. translate_ssh_variables() {
  271. local uname
  272. local home
  273. uname="$1"
  274. path="$2"
  275. # get the user's home directory
  276. userHome=$(getent passwd "$uname" | cut -d: -f6)
  277. # translate '%u' to user name
  278. path=${path/\%u/"$uname"}
  279. # translate '%h' to user home directory
  280. path=${path/\%h/"$userHome"}
  281. echo "$path"
  282. }
  283. # test that a string to conforms to GPG's expiration format
  284. test_gpg_expire() {
  285. echo "$1" | egrep -q "^[0-9]+[mwy]?$"
  286. }
  287. # check that a file is properly owned, and that all it's parent
  288. # directories are not group/other writable
  289. check_key_file_permissions() {
  290. local user
  291. local path
  292. local access
  293. local gAccess
  294. local oAccess
  295. # function to check that an octal corresponds to writability
  296. is_write() {
  297. [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
  298. }
  299. user="$1"
  300. path="$2"
  301. # return 0 is path does not exist
  302. [ -e "$path" ] || return 0
  303. owner=$(stat --format '%U' "$path")
  304. access=$(stat --format '%a' "$path")
  305. gAccess=$(echo "$access" | cut -c2)
  306. oAccess=$(echo "$access" | cut -c3)
  307. # check owner
  308. if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
  309. return 1
  310. fi
  311. # check group/other writability
  312. if is_write "$gAccess" || is_write "$oAccess" ; then
  313. return 2
  314. fi
  315. if [ "$path" = '/' ] ; then
  316. return 0
  317. else
  318. check_key_file_permissions $(dirname "$path")
  319. fi
  320. }
  321. ### CONVERSION UTILITIES
  322. # output the ssh key for a given key ID
  323. gpg2ssh() {
  324. local keyID
  325. keyID="$1"
  326. gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
  327. }
  328. # output known_hosts line from ssh key
  329. ssh2known_hosts() {
  330. local host
  331. local key
  332. host="$1"
  333. key="$2"
  334. echo -n "$host "
  335. echo -n "$key" | tr -d '\n'
  336. echo " MonkeySphere${DATE}"
  337. }
  338. # output authorized_keys line from ssh key
  339. ssh2authorized_keys() {
  340. local userID
  341. local key
  342. userID="$1"
  343. key="$2"
  344. echo -n "$key" | tr -d '\n'
  345. echo " MonkeySphere${DATE} ${userID}"
  346. }
  347. # convert key from gpg to ssh known_hosts format
  348. gpg2known_hosts() {
  349. local host
  350. local keyID
  351. host="$1"
  352. keyID="$2"
  353. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  354. # all lines in the known_hosts file. why?
  355. # NOTE: just in case, the COMMENT can be matched with the
  356. # following regexp:
  357. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  358. echo -n "$host "
  359. gpg2ssh "$keyID" | tr -d '\n'
  360. echo " MonkeySphere${DATE}"
  361. }
  362. # convert key from gpg to ssh authorized_keys format
  363. gpg2authorized_keys() {
  364. local userID
  365. local keyID
  366. userID="$1"
  367. keyID="$2"
  368. # NOTE: just in case, the COMMENT can be matched with the
  369. # following regexp:
  370. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  371. gpg2ssh "$keyID" | tr -d '\n'
  372. echo " MonkeySphere${DATE} ${userID}"
  373. }
  374. ### GPG UTILITIES
  375. # retrieve all keys with given user id from keyserver
  376. # FIXME: need to figure out how to retrieve all matching keys
  377. # (not just first N (5 in this case))
  378. gpg_fetch_userid() {
  379. local userID
  380. local returnCode
  381. if [ "$CHECK_KEYSERVER" != 'true' ] ; then
  382. return 0
  383. fi
  384. userID="$1"
  385. log info " checking keyserver $KEYSERVER... "
  386. echo 1,2,3,4,5 | \
  387. gpg --quiet --batch --with-colons \
  388. --command-fd 0 --keyserver "$KEYSERVER" \
  389. --search ="$userID" > /dev/null 2>&1
  390. returnCode="$?"
  391. # if the user is the monkeysphere user, then update the
  392. # monkeysphere user's trustdb
  393. if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
  394. gpg_authentication "--check-trustdb" > /dev/null 2>&1
  395. fi
  396. return "$returnCode"
  397. }
  398. ########################################################################
  399. ### PROCESSING FUNCTIONS
  400. # userid and key policy checking
  401. # the following checks policy on the returned keys
  402. # - checks that full key has appropriate valididy (u|f)
  403. # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
  404. # - checks that requested user ID has appropriate validity
  405. # (see /usr/share/doc/gnupg/DETAILS.gz)
  406. # output is one line for every found key, in the following format:
  407. #
  408. # flag:sshKey
  409. #
  410. # "flag" is an acceptability flag, 0 = ok, 1 = bad
  411. # "sshKey" is the translated gpg key
  412. #
  413. # all log output must go to stderr, as stdout is used to pass the
  414. # flag:sshKey to the calling function.
  415. #
  416. # expects global variable: "MODE"
  417. process_user_id() {
  418. local userID
  419. local requiredCapability
  420. local requiredPubCapability
  421. local gpgOut
  422. local type
  423. local validity
  424. local keyid
  425. local uidfpr
  426. local usage
  427. local keyOK
  428. local uidOK
  429. local lastKey
  430. local lastKeyOK
  431. local fingerprint
  432. userID="$1"
  433. # set the required key capability based on the mode
  434. if [ "$MODE" = 'known_hosts' ] ; then
  435. requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
  436. elif [ "$MODE" = 'authorized_keys' ] ; then
  437. requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
  438. fi
  439. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  440. # fetch the user ID if necessary/requested
  441. gpg_fetch_userid "$userID"
  442. # output gpg info for (exact) userid and store
  443. gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
  444. --with-fingerprint --with-fingerprint \
  445. ="$userID" 2>/dev/null)
  446. # if the gpg query return code is not 0, return 1
  447. if [ "$?" -ne 0 ] ; then
  448. log verbose " no primary keys found."
  449. return 1
  450. fi
  451. # loop over all lines in the gpg output and process.
  452. echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
  453. while IFS=: read -r type validity keyid uidfpr usage ; do
  454. # process based on record type
  455. case $type in
  456. 'pub') # primary keys
  457. # new key, wipe the slate
  458. keyOK=
  459. uidOK=
  460. lastKey=pub
  461. lastKeyOK=
  462. fingerprint=
  463. log verbose " primary key found: $keyid"
  464. # if overall key is not valid, skip
  465. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  466. log error " - unacceptable primary key validity ($validity)."
  467. continue
  468. fi
  469. # if overall key is disabled, skip
  470. if check_capability "$usage" 'D' ; then
  471. log error " - key disabled."
  472. continue
  473. fi
  474. # if overall key capability is not ok, skip
  475. if ! check_capability "$usage" $requiredPubCapability ; then
  476. log error " - unacceptable primary key capability ($usage)."
  477. continue
  478. fi
  479. # mark overall key as ok
  480. keyOK=true
  481. # mark primary key as ok if capability is ok
  482. if check_capability "$usage" $requiredCapability ; then
  483. lastKeyOK=true
  484. fi
  485. ;;
  486. 'uid') # user ids
  487. if [ "$lastKey" != pub ] ; then
  488. log error " - got a user ID after a sub key?! user IDs should only follow primary keys!"
  489. continue
  490. fi
  491. # if an acceptable user ID was already found, skip
  492. if [ "$uidOK" = 'true' ] ; then
  493. continue
  494. fi
  495. # if the user ID does matches...
  496. if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
  497. # and the user ID validity is ok
  498. if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
  499. # mark user ID acceptable
  500. uidOK=true
  501. fi
  502. else
  503. continue
  504. fi
  505. # output a line for the primary key
  506. # 0 = ok, 1 = bad
  507. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  508. log verbose " * acceptable primary key."
  509. if [ -z "$sshKey" ] ; then
  510. log error " ! primary key could not be translated (not RSA or DSA?)."
  511. else
  512. echo "0:${sshKey}"
  513. fi
  514. else
  515. log error " - unacceptable primary key."
  516. if [ -z "$sshKey" ] ; then
  517. log error " ! primary key could not be translated (not RSA or DSA?)."
  518. else
  519. echo "1:${sshKey}"
  520. fi
  521. fi
  522. ;;
  523. 'sub') # sub keys
  524. # unset acceptability of last key
  525. lastKey=sub
  526. lastKeyOK=
  527. fingerprint=
  528. # don't bother with sub keys if the primary key is not valid
  529. if [ "$keyOK" != true ] ; then
  530. continue
  531. fi
  532. # don't bother with sub keys if no user ID is acceptable:
  533. if [ "$uidOK" != true ] ; then
  534. continue
  535. fi
  536. # if sub key validity is not ok, skip
  537. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  538. continue
  539. fi
  540. # if sub key capability is not ok, skip
  541. if ! check_capability "$usage" $requiredCapability ; then
  542. continue
  543. fi
  544. # mark sub key as ok
  545. lastKeyOK=true
  546. ;;
  547. 'fpr') # key fingerprint
  548. fingerprint="$uidfpr"
  549. sshKey=$(gpg2ssh "$fingerprint")
  550. # if the last key was the pub key, skip
  551. if [ "$lastKey" = pub ] ; then
  552. continue
  553. fi
  554. # output a line for the sub key
  555. # 0 = ok, 1 = bad
  556. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  557. log verbose " * acceptable sub key."
  558. if [ -z "$sshKey" ] ; then
  559. log error " ! sub key could not be translated (not RSA or DSA?)."
  560. else
  561. echo "0:${sshKey}"
  562. fi
  563. else
  564. log error " - unacceptable sub key."
  565. if [ -z "$sshKey" ] ; then
  566. log error " ! sub key could not be translated (not RSA or DSA?)."
  567. else
  568. echo "1:${sshKey}"
  569. fi
  570. fi
  571. ;;
  572. esac
  573. done | sort -t: -k1 -n -r
  574. # NOTE: this last sort is important so that the "good" keys (key
  575. # flag '0') come last. This is so that they take precedence when
  576. # being processed in the key files over "bad" keys (key flag '1')
  577. }
  578. # process a single host in the known_host file
  579. process_host_known_hosts() {
  580. local host
  581. local userID
  582. local nKeys
  583. local nKeysOK
  584. local ok
  585. local sshKey
  586. local tmpfile
  587. host="$1"
  588. userID="ssh://${host}"
  589. log verbose "processing: $host"
  590. nKeys=0
  591. nKeysOK=0
  592. IFS=$'\n'
  593. for line in $(process_user_id "${userID}") ; do
  594. # note that key was found
  595. nKeys=$((nKeys+1))
  596. ok=$(echo "$line" | cut -d: -f1)
  597. sshKey=$(echo "$line" | cut -d: -f2)
  598. if [ -z "$sshKey" ] ; then
  599. continue
  600. fi
  601. # remove the old host key line, and note if removed
  602. remove_line "$KNOWN_HOSTS" "$sshKey"
  603. # if key OK, add new host line
  604. if [ "$ok" -eq '0' ] ; then
  605. # note that key was found ok
  606. nKeysOK=$((nKeysOK+1))
  607. # hash if specified
  608. if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
  609. # FIXME: this is really hackish cause ssh-keygen won't
  610. # hash from stdin to stdout
  611. tmpfile=$(mktemp)
  612. ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
  613. ssh-keygen -H -f "$tmpfile" 2> /dev/null
  614. cat "$tmpfile" >> "$KNOWN_HOSTS"
  615. rm -f "$tmpfile" "${tmpfile}.old"
  616. else
  617. ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
  618. fi
  619. fi
  620. done
  621. # if at least one key was found...
  622. if [ "$nKeys" -gt 0 ] ; then
  623. # if ok keys were found, return 0
  624. if [ "$nKeysOK" -gt 0 ] ; then
  625. return 0
  626. # else return 2
  627. else
  628. return 2
  629. fi
  630. # if no keys were found, return 1
  631. else
  632. return 1
  633. fi
  634. }
  635. # update the known_hosts file for a set of hosts listed on command
  636. # line
  637. update_known_hosts() {
  638. local nHosts
  639. local nHostsOK
  640. local nHostsBAD
  641. local fileCheck
  642. local host
  643. # the number of hosts specified on command line
  644. nHosts="$#"
  645. nHostsOK=0
  646. nHostsBAD=0
  647. # create a lockfile on known_hosts:
  648. lock create "$KNOWN_HOSTS"
  649. # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
  650. trap "lock remove $KNOWN_HOSTS" EXIT
  651. # note pre update file checksum
  652. fileCheck="$(file_hash "$KNOWN_HOSTS")"
  653. for host ; do
  654. # process the host
  655. process_host_known_hosts "$host"
  656. # note the result
  657. case "$?" in
  658. 0)
  659. nHostsOK=$((nHostsOK+1))
  660. ;;
  661. 2)
  662. nHostsBAD=$((nHostsBAD+1))
  663. ;;
  664. esac
  665. # touch the lockfile, for good measure.
  666. lock touch "$KNOWN_HOSTS"
  667. done
  668. # remove the lockfile and the trap
  669. lock remove "$KNOWN_HOSTS"
  670. trap - EXIT
  671. # note if the known_hosts file was updated
  672. if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
  673. log verbose "known_hosts file updated."
  674. fi
  675. # if an acceptable host was found, return 0
  676. if [ "$nHostsOK" -gt 0 ] ; then
  677. return 0
  678. # else if no ok hosts were found...
  679. else
  680. # if no bad host were found then no hosts were found at all,
  681. # and return 1
  682. if [ "$nHostsBAD" -eq 0 ] ; then
  683. return 1
  684. # else if at least one bad host was found, return 2
  685. else
  686. return 2
  687. fi
  688. fi
  689. }
  690. # process hosts from a known_hosts file
  691. process_known_hosts() {
  692. local hosts
  693. log verbose "processing known_hosts file..."
  694. hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
  695. if [ -z "$hosts" ] ; then
  696. log error "no hosts to process."
  697. return
  698. fi
  699. # take all the hosts from the known_hosts file (first
  700. # field), grep out all the hashed hosts (lines starting
  701. # with '|')...
  702. update_known_hosts $hosts
  703. }
  704. # process uids for the authorized_keys file
  705. process_uid_authorized_keys() {
  706. local userID
  707. local nKeys
  708. local nKeysOK
  709. local ok
  710. local sshKey
  711. userID="$1"
  712. log verbose "processing: $userID"
  713. nKeys=0
  714. nKeysOK=0
  715. IFS=$'\n'
  716. for line in $(process_user_id "$userID") ; do
  717. # note that key was found
  718. nKeys=$((nKeys+1))
  719. ok=$(echo "$line" | cut -d: -f1)
  720. sshKey=$(echo "$line" | cut -d: -f2)
  721. if [ -z "$sshKey" ] ; then
  722. continue
  723. fi
  724. # remove the old host key line
  725. remove_line "$AUTHORIZED_KEYS" "$sshKey"
  726. # if key OK, add new host line
  727. if [ "$ok" -eq '0' ] ; then
  728. # note that key was found ok
  729. nKeysOK=$((nKeysOK+1))
  730. ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
  731. fi
  732. done
  733. # if at least one key was found...
  734. if [ "$nKeys" -gt 0 ] ; then
  735. # if ok keys were found, return 0
  736. if [ "$nKeysOK" -gt 0 ] ; then
  737. return 0
  738. # else return 2
  739. else
  740. return 2
  741. fi
  742. # if no keys were found, return 1
  743. else
  744. return 1
  745. fi
  746. }
  747. # update the authorized_keys files from a list of user IDs on command
  748. # line
  749. update_authorized_keys() {
  750. local userID
  751. local nIDs
  752. local nIDsOK
  753. local nIDsBAD
  754. local fileCheck
  755. # the number of ids specified on command line
  756. nIDs="$#"
  757. nIDsOK=0
  758. nIDsBAD=0
  759. # create a lockfile on authorized_keys
  760. lock create "$AUTHORIZED_KEYS"
  761. # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
  762. trap "lock remove $AUTHORIZED_KEYS" EXIT
  763. # note pre update file checksum
  764. fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
  765. # remove any monkeysphere lines from authorized_keys file
  766. remove_monkeysphere_lines "$AUTHORIZED_KEYS"
  767. for userID ; do
  768. # process the user ID, change return code if key not found for
  769. # user ID
  770. process_uid_authorized_keys "$userID"
  771. # note the result
  772. case "$?" in
  773. 0)
  774. nIDsOK=$((nIDsOK+1))
  775. ;;
  776. 2)
  777. nIDsBAD=$((nIDsBAD+1))
  778. ;;
  779. esac
  780. # touch the lockfile, for good measure.
  781. lock touch "$AUTHORIZED_KEYS"
  782. done
  783. # remove the lockfile and the trap
  784. lock remove "$AUTHORIZED_KEYS"
  785. trap - EXIT
  786. # note if the authorized_keys file was updated
  787. if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
  788. log verbose "authorized_keys file updated."
  789. fi
  790. # if an acceptable id was found, return 0
  791. if [ "$nIDsOK" -gt 0 ] ; then
  792. return 0
  793. # else if no ok ids were found...
  794. else
  795. # if no bad ids were found then no ids were found at all, and
  796. # return 1
  797. if [ "$nIDsBAD" -eq 0 ] ; then
  798. return 1
  799. # else if at least one bad id was found, return 2
  800. else
  801. return 2
  802. fi
  803. fi
  804. }
  805. # process an authorized_user_ids file for authorized_keys
  806. process_authorized_user_ids() {
  807. local line
  808. local nline
  809. local userIDs
  810. authorizedUserIDs="$1"
  811. log verbose "processing authorized_user_ids file..."
  812. if ! meat "$authorizedUserIDs" > /dev/null ; then
  813. log error "no user IDs to process."
  814. return
  815. fi
  816. nline=0
  817. # extract user IDs from authorized_user_ids file
  818. IFS=$'\n'
  819. for line in $(meat "$authorizedUserIDs") ; do
  820. userIDs["$nline"]="$line"
  821. nline=$((nline+1))
  822. done
  823. update_authorized_keys "${userIDs[@]}"
  824. }