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