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