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