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