summaryrefslogtreecommitdiff
path: root/src/common
blob: 9dcc5e869986188cb58b4eef491f93aac1f9e3a7 (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. # 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. # 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. }