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