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