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