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