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