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