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