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