summaryrefslogtreecommitdiff
path: root/src/common
blob: 54ea9cb98c51a84200c3858f1e6666daef2ad7a4 (plain)
  1. # -*-shell-script-*-
  2. # Shared sh functions for the monkeysphere
  3. #
  4. # Written by
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. #
  7. # Copyright 2008, released under the GPL, version 3 or later
  8. # all-caps variables are meant to be user supplied (ie. from config
  9. # file) and are considered global
  10. ########################################################################
  11. ### COMMON VARIABLES
  12. # managed directories
  13. ETC="/etc/monkeysphere"
  14. export ETC
  15. ########################################################################
  16. ### UTILITY FUNCTIONS
  17. # failure function. exits with code 255, unless specified otherwise.
  18. failure() {
  19. echo "$1" >&2
  20. exit ${2:-'255'}
  21. }
  22. # write output to stderr
  23. log() {
  24. echo -n "ms: " >&2
  25. echo "$@" >&2
  26. }
  27. loge() {
  28. echo "$@" >&2
  29. }
  30. # cut out all comments(#) and blank lines from standard input
  31. meat() {
  32. grep -v -e "^[[:space:]]*#" -e '^$' "$1"
  33. }
  34. # cut a specified line from standard input
  35. cutline() {
  36. head --line="$1" "$2" | tail -1
  37. }
  38. # check that characters are in a string (in an AND fashion).
  39. # used for checking key capability
  40. # check_capability capability a [b...]
  41. check_capability() {
  42. local usage
  43. local capcheck
  44. usage="$1"
  45. shift 1
  46. for capcheck ; do
  47. if echo "$usage" | grep -q -v "$capcheck" ; then
  48. return 1
  49. fi
  50. done
  51. return 0
  52. }
  53. # hash of a file
  54. file_hash() {
  55. md5sum "$1" 2> /dev/null
  56. }
  57. # convert escaped characters in pipeline from gpg output back into
  58. # original character
  59. # FIXME: undo all escape character translation in with-colons gpg
  60. # output
  61. gpg_unescape() {
  62. sed 's/\\x3a/:/g'
  63. }
  64. # convert nasty chars into gpg-friendly form in pipeline
  65. # FIXME: escape everything, not just colons!
  66. gpg_escape() {
  67. sed 's/:/\\x3a/g'
  68. }
  69. # prompt for GPG-formatted expiration, and emit result on stdout
  70. get_gpg_expiration() {
  71. local keyExpire=
  72. cat >&2 <<EOF
  73. Please specify how long the key should be valid.
  74. 0 = key does not expire
  75. <n> = key expires in n days
  76. <n>w = key expires in n weeks
  77. <n>m = key expires in n months
  78. <n>y = key expires in n years
  79. EOF
  80. while [ -z "$keyExpire" ] ; do
  81. read -p "Key is valid for? (0) " keyExpire
  82. if ! test_gpg_expire ${keyExpire:=0} ; then
  83. echo "invalid value" >&2
  84. unset keyExpire
  85. fi
  86. done
  87. echo "$keyExpire"
  88. }
  89. # remove all lines with specified string from specified file
  90. remove_line() {
  91. local file
  92. local string
  93. file="$1"
  94. string="$2"
  95. if [ -z "$file" -o -z "$string" ] ; then
  96. return 1
  97. fi
  98. if [ ! -e "$file" ] ; then
  99. return 1
  100. fi
  101. # if the string is in the file...
  102. if grep -q -F "$string" "$file" 2> /dev/null ; then
  103. # remove the line with the string, and return 0
  104. grep -v -F "$string" "$file" | sponge "$file"
  105. return 0
  106. # otherwise return 1
  107. else
  108. return 1
  109. fi
  110. }
  111. # remove all lines with MonkeySphere strings in file
  112. remove_monkeysphere_lines() {
  113. local file
  114. file="$1"
  115. if [ -z "$file" ] ; then
  116. return 1
  117. fi
  118. if [ ! -e "$file" ] ; then
  119. return 1
  120. fi
  121. egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
  122. "$file" | sponge "$file"
  123. }
  124. # translate ssh-style path variables %h and %u
  125. translate_ssh_variables() {
  126. local uname
  127. local home
  128. uname="$1"
  129. path="$2"
  130. # get the user's home directory
  131. userHome=$(getent passwd "$uname" | cut -d: -f6)
  132. # translate '%u' to user name
  133. path=${path/\%u/"$uname"}
  134. # translate '%h' to user home directory
  135. path=${path/\%h/"$userHome"}
  136. echo "$path"
  137. }
  138. # test that a string to conforms to GPG's expiration format
  139. test_gpg_expire() {
  140. echo "$1" | egrep -q "^[0-9]+[mwy]?$"
  141. }
  142. # check that a file is properly owned, and that all it's parent
  143. # directories are not group/other writable
  144. check_key_file_permissions() {
  145. local user
  146. local path
  147. local access
  148. local gAccess
  149. local oAccess
  150. # function to check that an octal corresponds to writability
  151. is_write() {
  152. [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
  153. }
  154. user="$1"
  155. path="$2"
  156. # return 0 is path does not exist
  157. [ -e "$path" ] || return 0
  158. owner=$(stat --format '%U' "$path")
  159. access=$(stat --format '%a' "$path")
  160. gAccess=$(echo "$access" | cut -c2)
  161. oAccess=$(echo "$access" | cut -c3)
  162. # check owner
  163. if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
  164. return 1
  165. fi
  166. # check group/other writability
  167. if is_write "$gAccess" || is_write "$oAccess" ; then
  168. return 2
  169. fi
  170. if [ "$path" = '/' ] ; then
  171. return 0
  172. else
  173. check_key_file_permissions $(dirname "$path")
  174. fi
  175. }
  176. ### CONVERSION UTILITIES
  177. # output the ssh key for a given key ID
  178. gpg2ssh() {
  179. local keyID
  180. keyID="$1"
  181. gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
  182. }
  183. # output known_hosts line from ssh key
  184. ssh2known_hosts() {
  185. local host
  186. local key
  187. host="$1"
  188. key="$2"
  189. echo -n "$host "
  190. echo -n "$key" | tr -d '\n'
  191. echo " MonkeySphere${DATE}"
  192. }
  193. # output authorized_keys line from ssh key
  194. ssh2authorized_keys() {
  195. local userID
  196. local key
  197. userID="$1"
  198. key="$2"
  199. echo -n "$key" | tr -d '\n'
  200. echo " MonkeySphere${DATE} ${userID}"
  201. }
  202. # convert key from gpg to ssh known_hosts format
  203. gpg2known_hosts() {
  204. local host
  205. local keyID
  206. host="$1"
  207. keyID="$2"
  208. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  209. # all lines in the known_hosts file. why?
  210. # NOTE: just in case, the COMMENT can be matched with the
  211. # following regexp:
  212. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  213. echo -n "$host "
  214. gpg2ssh "$keyID" | tr -d '\n'
  215. echo " MonkeySphere${DATE}"
  216. }
  217. # convert key from gpg to ssh authorized_keys format
  218. gpg2authorized_keys() {
  219. local userID
  220. local keyID
  221. userID="$1"
  222. keyID="$2"
  223. # NOTE: just in case, the COMMENT can be matched with the
  224. # following regexp:
  225. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  226. gpg2ssh "$keyID" | tr -d '\n'
  227. echo " MonkeySphere${DATE} ${userID}"
  228. }
  229. ### GPG UTILITIES
  230. # retrieve all keys with given user id from keyserver
  231. # FIXME: need to figure out how to retrieve all matching keys
  232. # (not just first N (5 in this case))
  233. gpg_fetch_userid() {
  234. local userID
  235. local returnCode
  236. if [ "$CHECK_KEYSERVER" != 'true' ] ; then
  237. return 0
  238. fi
  239. userID="$1"
  240. log -n " checking keyserver $KEYSERVER... "
  241. echo 1,2,3,4,5 | \
  242. gpg --quiet --batch --with-colons \
  243. --command-fd 0 --keyserver "$KEYSERVER" \
  244. --search ="$userID" > /dev/null 2>&1
  245. returnCode="$?"
  246. loge "done."
  247. # if the user is the monkeysphere user, then update the
  248. # monkeysphere user's trustdb
  249. if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
  250. gpg_authentication "--check-trustdb" > /dev/null 2>&1
  251. fi
  252. return "$returnCode"
  253. }
  254. ########################################################################
  255. ### PROCESSING FUNCTIONS
  256. # userid and key policy checking
  257. # the following checks policy on the returned keys
  258. # - checks that full key has appropriate valididy (u|f)
  259. # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
  260. # - checks that requested user ID has appropriate validity
  261. # (see /usr/share/doc/gnupg/DETAILS.gz)
  262. # output is one line for every found key, in the following format:
  263. #
  264. # flag:fingerprint
  265. #
  266. # "flag" is an acceptability flag, 0 = ok, 1 = bad
  267. # "fingerprint" is the fingerprint of the key
  268. #
  269. # expects global variable: "MODE"
  270. process_user_id() {
  271. local userID
  272. local requiredCapability
  273. local requiredPubCapability
  274. local gpgOut
  275. local type
  276. local validity
  277. local keyid
  278. local uidfpr
  279. local usage
  280. local keyOK
  281. local uidOK
  282. local lastKey
  283. local lastKeyOK
  284. local fingerprint
  285. userID="$1"
  286. # set the required key capability based on the mode
  287. if [ "$MODE" = 'known_hosts' ] ; then
  288. requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
  289. elif [ "$MODE" = 'authorized_keys' ] ; then
  290. requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
  291. fi
  292. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  293. # fetch the user ID if necessary/requested
  294. gpg_fetch_userid "$userID"
  295. # output gpg info for (exact) userid and store
  296. gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
  297. --with-fingerprint --with-fingerprint \
  298. ="$userID" 2>/dev/null)
  299. # if the gpg query return code is not 0, return 1
  300. if [ "$?" -ne 0 ] ; then
  301. log " no primary keys found."
  302. return 1
  303. fi
  304. # loop over all lines in the gpg output and process.
  305. echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
  306. while IFS=: read -r type validity keyid uidfpr usage ; do
  307. # process based on record type
  308. case $type in
  309. 'pub') # primary keys
  310. # new key, wipe the slate
  311. keyOK=
  312. uidOK=
  313. lastKey=pub
  314. lastKeyOK=
  315. fingerprint=
  316. log " primary key found: $keyid"
  317. # if overall key is not valid, skip
  318. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  319. log " - unacceptable primary key validity ($validity)."
  320. continue
  321. fi
  322. # if overall key is disabled, skip
  323. if check_capability "$usage" 'D' ; then
  324. log " - key disabled."
  325. continue
  326. fi
  327. # if overall key capability is not ok, skip
  328. if ! check_capability "$usage" $requiredPubCapability ; then
  329. log " - unacceptable primary key capability ($usage)."
  330. continue
  331. fi
  332. # mark overall key as ok
  333. keyOK=true
  334. # mark primary key as ok if capability is ok
  335. if check_capability "$usage" $requiredCapability ; then
  336. lastKeyOK=true
  337. fi
  338. ;;
  339. 'uid') # user ids
  340. if [ "$lastKey" != pub ] ; then
  341. log " - got a user ID after a sub key?! user IDs should only follow primary keys!"
  342. continue
  343. fi
  344. # if an acceptable user ID was already found, skip
  345. if [ "$uidOK" = 'true' ] ; then
  346. continue
  347. fi
  348. # if the user ID does matches...
  349. if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
  350. # and the user ID validity is ok
  351. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  352. # mark user ID acceptable
  353. uidOK=true
  354. fi
  355. else
  356. continue
  357. fi
  358. # output a line for the primary key
  359. # 0 = ok, 1 = bad
  360. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  361. log " * acceptable primary key."
  362. if [ -z "$sshKey" ] ; then
  363. log " ! primary key could not be translated (not RSA or DSA?)."
  364. else
  365. echo "0:${sshKey}"
  366. fi
  367. else
  368. log " - unacceptable primary key."
  369. if [ -z "$sshKey" ] ; then
  370. log " ! primary key could not be translated (not RSA or DSA?)."
  371. else
  372. echo "1:${sshKey}"
  373. fi
  374. fi
  375. ;;
  376. 'sub') # sub keys
  377. # unset acceptability of last key
  378. lastKey=sub
  379. lastKeyOK=
  380. fingerprint=
  381. # don't bother with sub keys if the primary key is not valid
  382. if [ "$keyOK" != true ] ; then
  383. continue
  384. fi
  385. # don't bother with sub keys if no user ID is acceptable:
  386. if [ "$uidOK" != true ] ; then
  387. continue
  388. fi
  389. # if sub key validity is not ok, skip
  390. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  391. continue
  392. fi
  393. # if sub key capability is not ok, skip
  394. if ! check_capability "$usage" $requiredCapability ; then
  395. continue
  396. fi
  397. # mark sub key as ok
  398. lastKeyOK=true
  399. ;;
  400. 'fpr') # key fingerprint
  401. fingerprint="$uidfpr"
  402. sshKey=$(gpg2ssh "$fingerprint")
  403. # if the last key was the pub key, skip
  404. if [ "$lastKey" = pub ] ; then
  405. continue
  406. fi
  407. # output a line for the sub key
  408. # 0 = ok, 1 = bad
  409. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  410. log " * acceptable sub key."
  411. if [ -z "$sshKey" ] ; then
  412. log " ! sub key could not be translated (not RSA or DSA?)."
  413. else
  414. echo "0:${sshKey}"
  415. fi
  416. else
  417. log " - unacceptable sub key."
  418. if [ -z "$sshKey" ] ; then
  419. log " ! sub key could not be translated (not RSA or DSA?)."
  420. else
  421. echo "1:${sshKey}"
  422. fi
  423. fi
  424. ;;
  425. esac
  426. done | sort -t: -k1 -n -r
  427. # NOTE: this last sort is important so that the "good" keys (key
  428. # flag '0') come last. This is so that they take precedence when
  429. # being processed in the key files over "bad" keys (key flag '1')
  430. }
  431. # process a single host in the known_host file
  432. process_host_known_hosts() {
  433. local host
  434. local userID
  435. local nKeys
  436. local nKeysOK
  437. local ok
  438. local sshKey
  439. local tmpfile
  440. host="$1"
  441. userID="ssh://${host}"
  442. log "processing: $host"
  443. nKeys=0
  444. nKeysOK=0
  445. IFS=$'\n'
  446. for line in $(process_user_id "${userID}") ; do
  447. # note that key was found
  448. nKeys=$((nKeys+1))
  449. ok=$(echo "$line" | cut -d: -f1)
  450. sshKey=$(echo "$line" | cut -d: -f2)
  451. if [ -z "$sshKey" ] ; then
  452. continue
  453. fi
  454. # remove the old host key line, and note if removed
  455. remove_line "$KNOWN_HOSTS" "$sshKey"
  456. # if key OK, add new host line
  457. if [ "$ok" -eq '0' ] ; then
  458. # note that key was found ok
  459. nKeysOK=$((nKeysOK+1))
  460. # hash if specified
  461. if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
  462. # FIXME: this is really hackish cause ssh-keygen won't
  463. # hash from stdin to stdout
  464. tmpfile=$(mktemp)
  465. ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
  466. ssh-keygen -H -f "$tmpfile" 2> /dev/null
  467. cat "$tmpfile" >> "$KNOWN_HOSTS"
  468. rm -f "$tmpfile" "${tmpfile}.old"
  469. else
  470. ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
  471. fi
  472. fi
  473. done
  474. # if at least one key was found...
  475. if [ "$nKeys" -gt 0 ] ; then
  476. # if ok keys were found, return 0
  477. if [ "$nKeysOK" -gt 0 ] ; then
  478. return 0
  479. # else return 2
  480. else
  481. return 2
  482. fi
  483. # if no keys were found, return 1
  484. else
  485. return 1
  486. fi
  487. }
  488. # update the known_hosts file for a set of hosts listed on command
  489. # line
  490. update_known_hosts() {
  491. local nHosts
  492. local nHostsOK
  493. local nHostsBAD
  494. local fileCheck
  495. local host
  496. # the number of hosts specified on command line
  497. nHosts="$#"
  498. nHostsOK=0
  499. nHostsBAD=0
  500. # set the trap to remove any lockfiles on exit
  501. trap "lockfile-remove $KNOWN_HOSTS" EXIT
  502. # create a lockfile on known_hosts
  503. lockfile-create "$KNOWN_HOSTS"
  504. # note pre update file checksum
  505. fileCheck="$(file_hash "$KNOWN_HOSTS")"
  506. for host ; do
  507. # process the host
  508. process_host_known_hosts "$host"
  509. # note the result
  510. case "$?" in
  511. 0)
  512. nHostsOK=$((nHostsOK+1))
  513. ;;
  514. 2)
  515. nHostsBAD=$((nHostsBAD+1))
  516. ;;
  517. esac
  518. # touch the lockfile, for good measure.
  519. lockfile-touch --oneshot "$KNOWN_HOSTS"
  520. done
  521. # remove the lockfile
  522. lockfile-remove "$KNOWN_HOSTS"
  523. # note if the known_hosts file was updated
  524. if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
  525. log "known_hosts file updated."
  526. fi
  527. # if an acceptable host was found, return 0
  528. if [ "$nHostsOK" -gt 0 ] ; then
  529. return 0
  530. # else if no ok hosts were found...
  531. else
  532. # if no bad host were found then no hosts were found at all,
  533. # and return 1
  534. if [ "$nHostsBAD" -eq 0 ] ; then
  535. return 1
  536. # else if at least one bad host was found, return 2
  537. else
  538. return 2
  539. fi
  540. fi
  541. }
  542. # process hosts from a known_hosts file
  543. process_known_hosts() {
  544. local hosts
  545. log "processing known_hosts file..."
  546. hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
  547. if [ -z "$hosts" ] ; then
  548. log "no hosts to process."
  549. return
  550. fi
  551. # take all the hosts from the known_hosts file (first
  552. # field), grep out all the hashed hosts (lines starting
  553. # with '|')...
  554. update_known_hosts $hosts
  555. }
  556. # process uids for the authorized_keys file
  557. process_uid_authorized_keys() {
  558. local userID
  559. local nKeys
  560. local nKeysOK
  561. local ok
  562. local sshKey
  563. userID="$1"
  564. log "processing: $userID"
  565. nKeys=0
  566. nKeysOK=0
  567. IFS=$'\n'
  568. for line in $(process_user_id "$userID") ; do
  569. # note that key was found
  570. nKeys=$((nKeys+1))
  571. ok=$(echo "$line" | cut -d: -f1)
  572. sshKey=$(echo "$line" | cut -d: -f2)
  573. if [ -z "$sshKey" ] ; then
  574. continue
  575. fi
  576. # remove the old host key line
  577. remove_line "$AUTHORIZED_KEYS" "$sshKey"
  578. # if key OK, add new host line
  579. if [ "$ok" -eq '0' ] ; then
  580. # note that key was found ok
  581. nKeysOK=$((nKeysOK+1))
  582. ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
  583. fi
  584. done
  585. # if at least one key was found...
  586. if [ "$nKeys" -gt 0 ] ; then
  587. # if ok keys were found, return 0
  588. if [ "$nKeysOK" -gt 0 ] ; then
  589. return 0
  590. # else return 2
  591. else
  592. return 2
  593. fi
  594. # if no keys were found, return 1
  595. else
  596. return 1
  597. fi
  598. }
  599. # update the authorized_keys files from a list of user IDs on command
  600. # line
  601. update_authorized_keys() {
  602. local userID
  603. local nIDs
  604. local nIDsOK
  605. local nIDsBAD
  606. local fileCheck
  607. # the number of ids specified on command line
  608. nIDs="$#"
  609. nIDsOK=0
  610. nIDsBAD=0
  611. # set the trap to remove any lockfiles on exit
  612. trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
  613. # create a lockfile on authorized_keys
  614. lockfile-create "$AUTHORIZED_KEYS"
  615. # note pre update file checksum
  616. fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
  617. # remove any monkeysphere lines from authorized_keys file
  618. remove_monkeysphere_lines "$AUTHORIZED_KEYS"
  619. for userID ; do
  620. # process the user ID, change return code if key not found for
  621. # user ID
  622. process_uid_authorized_keys "$userID"
  623. # note the result
  624. case "$?" in
  625. 0)
  626. nIDsOK=$((nIDsOK+1))
  627. ;;
  628. 2)
  629. nIDsBAD=$((nIDsBAD+1))
  630. ;;
  631. esac
  632. # touch the lockfile, for good measure.
  633. lockfile-touch --oneshot "$AUTHORIZED_KEYS"
  634. done
  635. # remove the lockfile
  636. lockfile-remove "$AUTHORIZED_KEYS"
  637. # note if the authorized_keys file was updated
  638. if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
  639. log "authorized_keys file updated."
  640. fi
  641. # if an acceptable id was found, return 0
  642. if [ "$nIDsOK" -gt 0 ] ; then
  643. return 0
  644. # else if no ok ids were found...
  645. else
  646. # if no bad ids were found then no ids were found at all, and
  647. # return 1
  648. if [ "$nIDsBAD" -eq 0 ] ; then
  649. return 1
  650. # else if at least one bad id was found, return 2
  651. else
  652. return 2
  653. fi
  654. fi
  655. }
  656. # process an authorized_user_ids file for authorized_keys
  657. process_authorized_user_ids() {
  658. local line
  659. local nline
  660. local userIDs
  661. authorizedUserIDs="$1"
  662. log "processing authorized_user_ids file..."
  663. if ! meat "$authorizedUserIDs" > /dev/null ; then
  664. log "no user IDs to process."
  665. return
  666. fi
  667. nline=0
  668. # extract user IDs from authorized_user_ids file
  669. IFS=$'\n'
  670. for line in $(meat "$authorizedUserIDs") ; do
  671. userIDs["$nline"]="$line"
  672. nline=$((nline+1))
  673. done
  674. update_authorized_keys "${userIDs[@]}"
  675. }