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