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