summaryrefslogtreecommitdiff
path: root/src/share/common
blob: 6cf38f6ee0435d5d59e6eed77cef3fbb59936825 (plain)
  1. #!/usr/bin/env bash
  2. # -*-shell-script-*-
  3. # This should be sourced by bash (though we welcome changes to make it POSIX sh compliant)
  4. # Shared sh functions for the monkeysphere
  5. #
  6. # Written by
  7. # Jameson Rollins <jrollins@finestructure.net>
  8. # Jamie McClelland <jm@mayfirst.org>
  9. # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
  10. #
  11. # Copyright 2008-2009, released under the GPL, version 3 or later
  12. # all-caps variables are meant to be user supplied (ie. from config
  13. # file) and are considered global
  14. ########################################################################
  15. ### UTILITY FUNCTIONS
  16. # output version info
  17. version() {
  18. cat "${SYSSHAREDIR}/VERSION"
  19. }
  20. # failure function. exits with code 255, unless specified otherwise.
  21. failure() {
  22. [ "$1" ] && echo "$1" >&2
  23. exit ${2:-'255'}
  24. }
  25. # write output to stderr based on specified LOG_LEVEL the first
  26. # parameter is the priority of the output, and everything else is what
  27. # is echoed to stderr. If there is nothing else, then output comes
  28. # from stdin, and is not prefaced by log prefix.
  29. log() {
  30. local priority
  31. local level
  32. local output
  33. local alllevels
  34. local found=
  35. # don't include SILENT in alllevels: it's handled separately
  36. # list in decreasing verbosity (all caps).
  37. # separate with $IFS explicitly, since we do some fancy footwork
  38. # elsewhere.
  39. alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
  40. # translate lowers to uppers in global log level
  41. LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
  42. # just go ahead and return if the log level is silent
  43. if [ "$LOG_LEVEL" = 'SILENT' ] ; then
  44. return
  45. fi
  46. for level in $alllevels ; do
  47. if [ "$LOG_LEVEL" = "$level" ] ; then
  48. found=true
  49. fi
  50. done
  51. if [ -z "$found" ] ; then
  52. # default to INFO:
  53. LOG_LEVEL=INFO
  54. fi
  55. # get priority from first parameter, translating all lower to
  56. # uppers
  57. priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
  58. shift
  59. # scan over available levels
  60. for level in $alllevels ; do
  61. # output if the log level matches, set output to true
  62. # this will output for all subsequent loops as well.
  63. if [ "$LOG_LEVEL" = "$level" ] ; then
  64. output=true
  65. fi
  66. if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
  67. if [ "$1" ] ; then
  68. echo "$@"
  69. else
  70. cat
  71. fi | sed 's/^/'"${LOG_PREFIX}"'/' >&2
  72. fi
  73. done
  74. }
  75. # run command as monkeysphere user
  76. su_monkeysphere_user() {
  77. # our main goal here is to run the given command as the the
  78. # monkeysphere user, but without prompting for any sort of
  79. # authentication. If this is not possible, we should just fail.
  80. # FIXME: our current implementation is overly restrictive, because
  81. # there may be some su PAM configurations that would allow su
  82. # "$MONKEYSPHERE_USER" -c "$@" to Just Work without prompting,
  83. # allowing specific users to invoke commands which make use of
  84. # this user.
  85. # chpst (from runit) would be nice to use, but we don't want to
  86. # introduce an extra dependency just for this. This may be a
  87. # candidate for re-factoring if we switch implementation languages.
  88. # singlequote-escape strings - like this bashism:
  89. # printf -v CMDLINE "%q " "$@"
  90. local CMDLINE="$(perl -0 -e "foreach (@ARGV) {s/'/'\\\\''/g; print \"'\$_' \"}" "$@")"
  91. case "$(id -un)" in
  92. # if monkeysphere user, simply execute in current shell
  93. "$MONKEYSPHERE_USER")
  94. "$@"
  95. ;;
  96. # if root, su command as monkeysphere user
  97. 'root')
  98. su "$MONKEYSPHERE_USER" -c "$CMDLINE"
  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 directory
  115. msmktempdir() {
  116. mktemp -d ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
  117. }
  118. # make a temporary file
  119. msmktempfile() {
  120. mktemp ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
  121. }
  122. # this is a wrapper for doing lock functions.
  123. #
  124. # it lets us depend on either lockfile-progs (preferred) or procmail's
  125. # lockfile, and should
  126. lock() {
  127. local use_lockfileprogs=true
  128. local action="$1"
  129. local file="$2"
  130. if ! ( type lockfile-create &>/dev/null ) ; then
  131. if ! ( type lockfile &>/dev/null ); then
  132. failure "Neither lockfile-create nor lockfile are in the path!"
  133. fi
  134. use_lockfileprogs=
  135. fi
  136. case "$action" in
  137. create)
  138. if [ -n "$use_lockfileprogs" ] ; then
  139. lockfile-create "$file" || failure "unable to lock '$file'"
  140. else
  141. lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
  142. fi
  143. log debug "lock created on '$file'."
  144. ;;
  145. touch)
  146. if [ -n "$use_lockfileprogs" ] ; then
  147. lockfile-touch --oneshot "$file"
  148. else
  149. : Nothing to do here
  150. fi
  151. log debug "lock touched on '$file'."
  152. ;;
  153. remove)
  154. if [ -n "$use_lockfileprogs" ] ; then
  155. lockfile-remove "$file"
  156. else
  157. rm -f "${file}.lock"
  158. fi
  159. log debug "lock removed on '$file'."
  160. ;;
  161. *)
  162. failure "bad argument for lock subfunction '$action'"
  163. esac
  164. }
  165. # for portability, between gnu date and BSD date.
  166. # arguments should be: number longunits format
  167. # e.g. advance_date 20 seconds +%F
  168. advance_date() {
  169. local gnutry
  170. local number="$1"
  171. local longunits="$2"
  172. local format="$3"
  173. local shortunits
  174. # try things the GNU way first
  175. if date -d "$number $longunits" "$format" &>/dev/null; then
  176. date -d "$number $longunits" "$format"
  177. else
  178. # otherwise, convert to (a limited version of) BSD date syntax:
  179. case "$longunits" in
  180. years)
  181. shortunits=y
  182. ;;
  183. months)
  184. shortunits=m
  185. ;;
  186. weeks)
  187. shortunits=w
  188. ;;
  189. days)
  190. shortunits=d
  191. ;;
  192. hours)
  193. shortunits=H
  194. ;;
  195. minutes)
  196. shortunits=M
  197. ;;
  198. seconds)
  199. shortunits=S
  200. ;;
  201. *)
  202. # this is a longshot, and will likely fail; oh well.
  203. shortunits="$longunits"
  204. esac
  205. date "-v+${number}${shortunits}" "$format"
  206. fi
  207. }
  208. print_date_from_seconds_since_the_epoch() {
  209. local seconds="$1"
  210. local gnutry
  211. if ! date '+%F %T' -d @"${seconds}" 2>/dev/null ; then
  212. # try it the BSD date way:
  213. date -r "${seconds}" '+%F %T'
  214. fi
  215. }
  216. # check that characters are in a string (in an AND fashion).
  217. # used for checking key capability
  218. # check_capability capability a [b...]
  219. check_capability() {
  220. local usage
  221. local capcheck
  222. usage="$1"
  223. shift 1
  224. for capcheck ; do
  225. if echo "$usage" | grep -q -v "$capcheck" ; then
  226. return 1
  227. fi
  228. done
  229. return 0
  230. }
  231. # hash of a file
  232. file_hash() {
  233. if type md5sum &>/dev/null ; then
  234. md5sum "$1"
  235. elif type md5 &>/dev/null ; then
  236. md5 "$1"
  237. else
  238. failure "Neither md5sum nor md5 are in the path!"
  239. fi
  240. }
  241. # convert escaped characters in pipeline from gpg output back into
  242. # original character
  243. # FIXME: undo all escape character translation in with-colons gpg
  244. # output
  245. gpg_unescape() {
  246. sed 's/\\x3a/:/g'
  247. }
  248. # convert nasty chars into gpg-friendly form in pipeline
  249. # FIXME: escape everything, not just colons!
  250. gpg_escape() {
  251. sed 's/:/\\x3a/g'
  252. }
  253. # prompt for GPG-formatted expiration, and emit result on stdout
  254. get_gpg_expiration() {
  255. local keyExpire
  256. keyExpire="$1"
  257. if [ -z "$keyExpire" -a "$PROMPT" != 'false' ]; then
  258. cat >&2 <<EOF
  259. Please specify how long the key should be valid.
  260. 0 = key does not expire
  261. <n> = key expires in n days
  262. <n>w = key expires in n weeks
  263. <n>m = key expires in n months
  264. <n>y = key expires in n years
  265. EOF
  266. while [ -z "$keyExpire" ] ; do
  267. printf "Key is valid for? (0) " >&2
  268. read keyExpire
  269. if ! test_gpg_expire ${keyExpire:=0} ; then
  270. echo "invalid value" >&2
  271. unset keyExpire
  272. fi
  273. done
  274. elif ! test_gpg_expire "$keyExpire" ; then
  275. failure "invalid key expiration value '$keyExpire'."
  276. fi
  277. echo "$keyExpire"
  278. }
  279. passphrase_prompt() {
  280. local prompt="$1"
  281. local fifo="$2"
  282. local PASS
  283. if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null 2>/dev/null; then
  284. printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
  285. printf '(with prompt "%s")\n' "$prompt" | log debug
  286. "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
  287. else
  288. read -s -p "$prompt" PASS
  289. # Uses the builtin echo, so should not put the passphrase into
  290. # the process table. I think. --dkg
  291. echo "$PASS" > "$fifo"
  292. fi
  293. }
  294. # remove all lines with specified string from specified file
  295. remove_line() {
  296. local file
  297. local lines
  298. local tempfile
  299. file="$1"
  300. shift
  301. if [ ! -e "$file" ] ; then
  302. return 1
  303. fi
  304. if (($# == 1)) ; then
  305. lines=$(grep -F "$1" "$file") || true
  306. else
  307. lines=$(grep -F "$1" "$file" | grep -F "$2") || true
  308. fi
  309. # if the string was found, remove it
  310. if [ "$lines" ] ; then
  311. log debug "removing matching key lines..."
  312. tempfile=$(mktemp "${file}.XXXXXXX") || \
  313. failure "Unable to make temp file '${file}.XXXXXXX'"
  314. grep -v -x -F "$lines" "$file" >"$tempfile" || :
  315. mv -f "$tempfile" "$file"
  316. fi
  317. }
  318. # remove all lines with MonkeySphere strings from stdin
  319. remove_monkeysphere_lines() {
  320. egrep -v ' MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2} '
  321. }
  322. # translate ssh-style path variables %h and %u
  323. translate_ssh_variables() {
  324. local uname
  325. local home
  326. uname="$1"
  327. path="$2"
  328. # get the user's home directory
  329. userHome=$(get_homedir "$uname")
  330. # translate '%u' to user name
  331. path=${path/\%u/"$uname"}
  332. # translate '%h' to user home directory
  333. path=${path/\%h/"$userHome"}
  334. echo "$path"
  335. }
  336. # test that a string to conforms to GPG's expiration format
  337. test_gpg_expire() {
  338. echo "$1" | egrep -q "^[0-9]+[mwy]?$"
  339. }
  340. # touch a key file if it doesn't exist, including creating needed
  341. # directories with correct permissions
  342. touch_key_file_or_fail() {
  343. local keyFile="$1"
  344. local newUmask
  345. if [ ! -f "$keyFile" ]; then
  346. # make sure to create files and directories with the
  347. # appropriate write bits turned off:
  348. newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
  349. [ -d $(dirname "$keyFile") ] \
  350. || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$keyFile") ) \
  351. || failure "Could not create path to $keyFile"
  352. # make sure to create this file with the appropriate bits turned off:
  353. (umask "$newUmask" && touch "$keyFile") \
  354. || failure "Unable to create $keyFile"
  355. fi
  356. }
  357. # check that a file is properly owned, and that all it's parent
  358. # directories are not group/other writable
  359. check_key_file_permissions() {
  360. local uname
  361. local path
  362. uname="$1"
  363. path="$2"
  364. if [ "$STRICT_MODES" = 'false' ] ; then
  365. log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
  366. return 0
  367. fi
  368. log debug "checking path permission '$path'..."
  369. "${SYSSHAREDIR}/checkperms" "$uname" "$path"
  370. }
  371. # return a list of all users on the system
  372. list_users() {
  373. if type getent &>/dev/null ; then
  374. # for linux and FreeBSD systems
  375. getent passwd | cut -d: -f1
  376. elif type dscl &>/dev/null ; then
  377. # for Darwin systems
  378. dscl localhost -list /Search/Users
  379. else
  380. failure "Neither getent or dscl is in the path! Could not determine list of users."
  381. fi
  382. }
  383. # take one argument, a service name. in response, print a series of
  384. # lines, each with a unique numeric port number that might be
  385. # associated with that service name. (e.g. in: "https", out: "443")
  386. # if nothing is found, print nothing, and return 0.
  387. #
  388. # return 1 if there was an error in the search somehow
  389. get_port_for_service() {
  390. [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
  391. failure $(printf "This is not a valid service name: '%s'" "$1")
  392. if type getent &>/dev/null ; then
  393. # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
  394. (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
  395. elif [ -r /etc/services ] ; then
  396. # fall back to /etc/services for systems that don't have getent (MacOS?)
  397. # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
  398. awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
  399. else
  400. return 1
  401. fi
  402. }
  403. # return the path to the home directory of a user
  404. get_homedir() {
  405. local uname=${1:-`whoami`}
  406. eval "echo ~${uname}"
  407. }
  408. # return the primary group of a user
  409. get_primary_group() {
  410. local uname=${1:-`whoami`}
  411. groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
  412. }
  413. ### CONVERSION UTILITIES
  414. # output the ssh key for a given key ID
  415. gpg2ssh() {
  416. local keyID
  417. keyID="$1"
  418. gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
  419. }
  420. # output known_hosts line from ssh key
  421. ssh2known_hosts() {
  422. local host
  423. local port
  424. local key
  425. # FIXME this does not properly deal with IPv6 hosts using the
  426. # standard port (because it's unclear whether their final
  427. # colon-delimited address section is a port number or an address
  428. # string)
  429. host=${1%:*}
  430. port=${1##*:}
  431. key="$2"
  432. # specify the host and port properly for new ssh known_hosts
  433. # format
  434. if [ "$port" != "$host" ] ; then
  435. host="[${host}]:${port}"
  436. fi
  437. # hash if specified
  438. if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
  439. if (type ssh-keygen >/dev/null) ; then
  440. log verbose "hashing known_hosts line"
  441. # FIXME: this is really hackish cause
  442. # ssh-keygen won't hash from stdin to
  443. # stdout
  444. tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
  445. printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE" \
  446. > "$tmpfile"
  447. ssh-keygen -H -f "$tmpfile" 2>/dev/null
  448. if [[ "$keyFile" == '-' ]] ; then
  449. cat "$tmpfile"
  450. else
  451. cat "$tmpfile" >> "$keyFile"
  452. fi
  453. rm -f "$tmpfile" "${tmpfile}.old"
  454. # FIXME: we could do this without needing ssh-keygen.
  455. # hashed known_hosts looks like: |1|X|Y where 1 means SHA1
  456. # (nothing else is defined in openssh sources), X is the
  457. # salt (same length as the digest output), base64-encoded,
  458. # and Y is the digested hostname (also base64-encoded).
  459. # see hostfile.{c,h} in openssh sources.
  460. else
  461. log error "Cannot hash known_hosts line as requested."
  462. fi
  463. else
  464. printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
  465. fi
  466. }
  467. # output authorized_keys line from ssh key
  468. ssh2authorized_keys() {
  469. local userID="$1"
  470. local key="$2"
  471. if [[ "$AUTHORIZED_KEYS_OPTIONS" ]]; then
  472. printf "%s %s MonkeySphere%s %s\n" "$AUTHORIZED_KEYS_OPTIONS" "$key" "$DATE" "$userID"
  473. else
  474. printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
  475. fi
  476. }
  477. # convert key from gpg to ssh known_hosts format
  478. gpg2known_hosts() {
  479. local host
  480. local keyID
  481. local key
  482. host="$1"
  483. keyID="$2"
  484. key=$(gpg2ssh "$keyID")
  485. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  486. # all lines in the known_hosts file. why?
  487. # NOTE: just in case, the COMMENT can be matched with the
  488. # following regexp:
  489. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  490. printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
  491. }
  492. # convert key from gpg to ssh authorized_keys format
  493. gpg2authorized_keys() {
  494. local userID
  495. local keyID
  496. local key
  497. userID="$1"
  498. keyID="$2"
  499. key=$(gpg2ssh "$keyID")
  500. # NOTE: just in case, the COMMENT can be matched with the
  501. # following regexp:
  502. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  503. printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
  504. }
  505. ### GPG UTILITIES
  506. # script to determine if gpg version is equal to or greater than specified version
  507. is_gpg_version_greater_equal() {
  508. local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
  509. local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
  510. | tr '.' ' ' | sort -g -k1 -k2 -k3 \
  511. | tail -1 | tr ' ' '.')
  512. [[ "$gpgVersion" == "$latest" ]]
  513. }
  514. # retrieve all keys with given user id from keyserver
  515. gpg_fetch_userid() {
  516. local returnCode=0
  517. local userID
  518. local foundkeyids
  519. if [ "$CHECK_KEYSERVER" != 'true' ] ; then
  520. return 0
  521. fi
  522. userID="$1"
  523. log verbose " checking keyserver $KEYSERVER... "
  524. foundkeyids="$(echo | \
  525. gpg --quiet --batch --with-colons \
  526. --command-fd 0 --keyserver "$KEYSERVER" \
  527. --search ="$userID" 2>/dev/null)"
  528. returnCode="$?"
  529. if [ "$returnCode" != 0 ] ; then
  530. log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
  531. else
  532. log debug " keyserver raw output:
  533. -----
  534. $foundkeyids
  535. -----"
  536. foundkeyids="$(printf "%s" "$foundkeyids" | grep '^pub:' | cut -f2 -d: | sed 's/^/0x/')"
  537. log verbose " Found keyids on keyserver: $(printf "%s" "$foundkeyids" | tr '\n' ' ')"
  538. if [ -n "$foundkeyids" ]; then
  539. echo | gpg --quiet --batch --with-colons \
  540. --command-fd 0 --keyserver "$KEYSERVER" \
  541. --recv-keys $foundkeyids &>/dev/null
  542. returnCode="$?"
  543. if [ "$returnCode" != 0 ] ; then
  544. log error "Failure ($returnCode) receiving keyids ($foundkeyids) from keyserver $KEYSERVER"
  545. fi
  546. fi
  547. fi
  548. return "$returnCode"
  549. }
  550. ########################################################################
  551. ### PROCESSING FUNCTIONS
  552. # userid and key policy checking
  553. # the following checks policy on the returned keys
  554. # - checks that full key has appropriate valididy (u|f)
  555. # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
  556. # - checks that requested user ID has appropriate validity
  557. # (see /usr/share/doc/gnupg/DETAILS.gz)
  558. # output is one line for every found key, in the following format:
  559. #
  560. # flag:sshKey
  561. #
  562. # "flag" is an acceptability flag, 0 = ok, 1 = bad
  563. # "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
  564. #
  565. # all log output must go to stderr, as stdout is used to pass the
  566. # flag:sshKey to the calling function.
  567. process_user_id() {
  568. local returnCode=0
  569. local userID="$1"
  570. local requiredCapability
  571. local requiredPubCapability
  572. local gpgOut
  573. local type
  574. local validity
  575. local keyid
  576. local uidfpr
  577. local usage
  578. local keyOK
  579. local uidOK
  580. local lastKey
  581. local lastKeyOK
  582. local fingerprint
  583. # set the required key capability based on the mode
  584. requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
  585. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  586. # fetch the user ID if necessary/requested
  587. gpg_fetch_userid "$userID"
  588. # output gpg info for (exact) userid and store
  589. gpgOut=$(gpg --list-key --fixed-list-mode --with-colons \
  590. --with-fingerprint --with-fingerprint \
  591. ="$userID" 2>/dev/null) || returnCode="$?"
  592. # if the gpg query return code is not 0, return 1
  593. if [ "$returnCode" -ne 0 ] ; then
  594. log verbose " no primary keys found."
  595. return 1
  596. fi
  597. # loop over all lines in the gpg output and process.
  598. echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
  599. while IFS=: read -r type validity keyid uidfpr usage ; do
  600. # process based on record type
  601. case $type in
  602. 'pub') # primary keys
  603. # new key, wipe the slate
  604. keyOK=
  605. uidOK=
  606. lastKey=pub
  607. lastKeyOK=
  608. fingerprint=
  609. log verbose " primary key found: $keyid"
  610. # if overall key is not valid, skip
  611. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  612. log debug " - unacceptable primary key validity ($validity)."
  613. continue
  614. fi
  615. # if overall key is disabled, skip
  616. if check_capability "$usage" 'D' ; then
  617. log debug " - key disabled."
  618. continue
  619. fi
  620. # if overall key capability is not ok, skip
  621. if ! check_capability "$usage" $requiredPubCapability ; then
  622. log debug " - unacceptable primary key capability ($usage)."
  623. continue
  624. fi
  625. # mark overall key as ok
  626. keyOK=true
  627. # mark primary key as ok if capability is ok
  628. if check_capability "$usage" $requiredCapability ; then
  629. lastKeyOK=true
  630. fi
  631. ;;
  632. 'uid') # user ids
  633. if [ "$lastKey" != pub ] ; then
  634. log verbose " ! got a user ID after a sub key?! user IDs should only follow primary keys!"
  635. continue
  636. fi
  637. # if an acceptable user ID was already found, skip
  638. if [ "$uidOK" = 'true' ] ; then
  639. continue
  640. fi
  641. # if the user ID does matches...
  642. if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
  643. # and the user ID validity is ok
  644. if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
  645. # mark user ID acceptable
  646. uidOK=true
  647. else
  648. log debug " - unacceptable user ID validity ($validity)."
  649. fi
  650. else
  651. continue
  652. fi
  653. # output a line for the primary key
  654. # 0 = ok, 1 = bad
  655. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  656. log verbose " * acceptable primary key."
  657. if [ -z "$sshKey" ] ; then
  658. log verbose " ! primary key could not be translated (not RSA?)."
  659. else
  660. echo "0:${sshKey}"
  661. fi
  662. else
  663. log debug " - unacceptable primary key."
  664. if [ -z "$sshKey" ] ; then
  665. log debug " ! primary key could not be translated (not RSA?)."
  666. else
  667. echo "1:${sshKey}"
  668. fi
  669. fi
  670. ;;
  671. 'sub') # sub keys
  672. # unset acceptability of last key
  673. lastKey=sub
  674. lastKeyOK=
  675. fingerprint=
  676. # don't bother with sub keys if the primary key is not valid
  677. if [ "$keyOK" != true ] ; then
  678. continue
  679. fi
  680. # don't bother with sub keys if no user ID is acceptable:
  681. if [ "$uidOK" != true ] ; then
  682. continue
  683. fi
  684. # if sub key validity is not ok, skip
  685. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  686. log debug " - unacceptable sub key validity ($validity)."
  687. continue
  688. fi
  689. # if sub key capability is not ok, skip
  690. if ! check_capability "$usage" $requiredCapability ; then
  691. log debug " - unacceptable sub key capability ($usage)."
  692. continue
  693. fi
  694. # mark sub key as ok
  695. lastKeyOK=true
  696. ;;
  697. 'fpr') # key fingerprint
  698. fingerprint="$uidfpr"
  699. sshKey=$(gpg2ssh "$fingerprint")
  700. # if the last key was the pub key, skip
  701. if [ "$lastKey" = pub ] ; then
  702. continue
  703. fi
  704. # output a line for the sub key
  705. # 0 = ok, 1 = bad
  706. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  707. log verbose " * acceptable sub key."
  708. if [ -z "$sshKey" ] ; then
  709. log error " ! sub key could not be translated (not RSA?)."
  710. else
  711. echo "0:${sshKey}"
  712. fi
  713. else
  714. log debug " - unacceptable sub key."
  715. if [ -z "$sshKey" ] ; then
  716. log debug " ! sub key could not be translated (not RSA?)."
  717. else
  718. echo "1:${sshKey}"
  719. fi
  720. fi
  721. ;;
  722. esac
  723. done | sort -t: -k1 -n -r
  724. # NOTE: this last sort is important so that the "good" keys (key
  725. # flag '0') come last. This is so that they take precedence when
  726. # being processed in the key files over "bad" keys (key flag '1')
  727. }
  728. process_keys_for_file() {
  729. local keyFile="$1"
  730. local userID="$2"
  731. local host
  732. local ok
  733. local sshKey
  734. local keyLine
  735. log verbose "processing: $userID"
  736. log debug "key file: $keyFile"
  737. IFS=$'\n'
  738. for line in $(process_user_id "$userID") ; do
  739. ok=${line%%:*}
  740. sshKey=${line#*:}
  741. if [ -z "$sshKey" ] ; then
  742. continue
  743. fi
  744. # remove the old key line
  745. if [[ "$keyFile" != '-' ]] ; then
  746. case "$FILE_TYPE" in
  747. ('authorized_keys')
  748. remove_line "$keyFile" "$sshKey"
  749. ;;
  750. ('known_hosts')
  751. host=${userID#ssh://}
  752. remove_line "$keyFile" "$host" "$sshKey"
  753. ;;
  754. esac
  755. fi
  756. ((++KEYS_PROCESSED))
  757. # if key OK, add new key line
  758. if [ "$ok" -eq '0' ] ; then
  759. case "$FILE_TYPE" in
  760. ('raw')
  761. keyLine="$sshKey"
  762. ;;
  763. ('authorized_keys')
  764. keyLine=$(ssh2authorized_keys "$userID" "$sshKey")
  765. ;;
  766. ('known_hosts')
  767. host=${userID#ssh://}
  768. keyLine=$(ssh2known_hosts "$host" "$sshKey")
  769. ;;
  770. esac
  771. echo "key line: $keyLine" | log debug
  772. if [[ "$keyFile" == '-' ]] ; then
  773. echo "$keyLine"
  774. else
  775. log debug "adding key line to file..."
  776. echo "$keyLine" >>"$keyFile"
  777. fi
  778. ((++KEYS_VALID))
  779. fi
  780. done
  781. log debug "KEYS_PROCESSED=$KEYS_PROCESSED"
  782. log debug "KEYS_VALID=$KEYS_VALID"
  783. }
  784. # process an authorized_user_ids file on stdin for authorized_keys
  785. process_authorized_user_ids() {
  786. local authorizedKeys="$1"
  787. declare -i nline=0
  788. local line
  789. declare -a userIDs
  790. declare -a koptions
  791. # extract user IDs from authorized_user_ids file
  792. IFS=$'\n'
  793. while read line ; do
  794. case "$line" in
  795. ("#"*)
  796. continue
  797. ;;
  798. (" "*|$'\t'*)
  799. if [[ -z ${koptions[${nline}]} ]]; then
  800. koptions[${nline}]=$(echo $line | sed 's/^[ ]*//;s/[ ]$//;')
  801. else
  802. koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[ ]*//;s/[ ]$//;')"
  803. fi
  804. ;;
  805. (*)
  806. ((++nline))
  807. userIDs[${nline}]="$line"
  808. unset koptions[${nline}] || true
  809. ;;
  810. esac
  811. done
  812. for i in $(seq 1 $nline); do
  813. AUTHORIZED_KEYS_OPTIONS="${koptions[$i]}" FILE_TYPE='authorized_keys' process_keys_for_file "$authorizedKeys" "${userIDs[$i]}" || returnCode="$?"
  814. done
  815. }
  816. # takes a gpg key or keys on stdin, and outputs a list of
  817. # fingerprints, one per line:
  818. list_primary_fingerprints() {
  819. local fake=$(msmktempdir)
  820. trap "rm -rf $fake" EXIT
  821. GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
  822. GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
  823. awk -F: '/^fpr:/{ print $10 }'
  824. trap - EXIT
  825. rm -rf "$fake"
  826. }
  827. # takes an OpenPGP key or set of keys on stdin, a fingerprint or other
  828. # key identifier as $1, and outputs the gpg-formatted information for
  829. # the requested keys from the material on stdin
  830. get_cert_info() {
  831. local fake=$(msmktempdir)
  832. trap "rm -rf $fake" EXIT
  833. GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
  834. GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
  835. trap - EXIT
  836. rm -rf "$fake"
  837. }
  838. check_cruft_file() {
  839. local loc="$1"
  840. local version="$2"
  841. if [ -e "$loc" ] ; then
  842. printf "! The file '%s' is no longer used by\n monkeysphere (as of version %s), and can be removed.\n\n" "$loc" "$version" | log info
  843. fi
  844. }
  845. check_upgrade_dir() {
  846. local loc="$1"
  847. local version="$2"
  848. if [ -d "$loc" ] ; then
  849. printf "The presence of directory '%s' indicates that you have\nnot yet completed a monkeysphere upgrade.\nYou should probably run the following script:\n %s/transitions/%s\n\n" "$loc" "$SYSSHAREDIR" "$version" | log info
  850. fi
  851. }
  852. ## look for cruft from old versions of the monkeysphere, and notice if
  853. ## upgrades have not been run:
  854. report_cruft() {
  855. check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
  856. check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
  857. check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
  858. check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
  859. local found=
  860. for foo in "${SYSDATADIR}/backup-from-"*"-transition" ; do
  861. if [ -d "$foo" ] ; then
  862. printf "! %s\n" "$foo" | log info
  863. found=true
  864. fi
  865. done
  866. if [ "$found" ] ; then
  867. printf "The directories above are backups left over from a monkeysphere transition.\nThey may contain copies of sensitive data (host keys, certifier lists), but\nthey are no longer needed by monkeysphere.\nYou may remove them at any time.\n\n" | log info
  868. fi
  869. }
  870. if [ -n "$1" ] && [ "$(type -t "$1" || true)" = "function" ]; then
  871. "$@"
  872. fi