summaryrefslogtreecommitdiff
path: root/src/common
blob: 00ee7b04a124db6679959c35b5c000b2e9ea2389 (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. failure() {
  20. echo "$1" >&2
  21. exit ${2:-'1'}
  22. }
  23. # write output to stderr
  24. log() {
  25. echo -n "ms: " 1>&2
  26. echo "$@" 1>&2
  27. }
  28. loge() {
  29. echo "$@" 1>&2
  30. }
  31. # cut out all comments(#) and blank lines from standard input
  32. meat() {
  33. grep -v -e "^[[:space:]]*#" -e '^$'
  34. }
  35. # cut a specified line from standard input
  36. cutline() {
  37. head --line="$1" | tail -1
  38. }
  39. # check that characters are in a string (in an AND fashion).
  40. # used for checking key capability
  41. # check_capability capability a [b...]
  42. check_capability() {
  43. local usage
  44. local capcheck
  45. usage="$1"
  46. shift 1
  47. for capcheck ; do
  48. if echo "$usage" | grep -q -v "$capcheck" ; then
  49. return 1
  50. fi
  51. done
  52. return 0
  53. }
  54. # convert escaped characters from gpg output back into original
  55. # character
  56. # FIXME: undo all escape character translation in with-colons gpg output
  57. unescape() {
  58. echo "$1" | sed 's/\\x3a/:/'
  59. }
  60. # remove all lines with specified string from specified file
  61. remove_line() {
  62. local file
  63. local string
  64. file="$1"
  65. string="$2"
  66. if [ "$file" -a "$string" ] ; then
  67. grep -v "$string" "$file" | sponge "$file"
  68. fi
  69. }
  70. # translate ssh-style path variables %h and %u
  71. translate_ssh_variables() {
  72. local uname
  73. local home
  74. uname="$1"
  75. path="$2"
  76. # get the user's home directory
  77. userHome=$(getent passwd "$uname" | cut -d: -f6)
  78. # translate ssh-style path variables
  79. path=${path/\%u/"$uname"}
  80. path=${path/\%h/"$userHome"}
  81. echo "$path"
  82. }
  83. ### CONVERTION UTILITIES
  84. # output the ssh key for a given key ID
  85. gpg2ssh() {
  86. local keyID
  87. #keyID="$1" #TMP
  88. # only use last 16 characters until openpgp2ssh can take all 40 #TMP
  89. keyID=$(echo "$1" | cut -c 25-) #TMP
  90. gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
  91. }
  92. # output known_hosts line from ssh key
  93. ssh2known_hosts() {
  94. local host
  95. local key
  96. host="$1"
  97. key="$2"
  98. echo -n "$host "
  99. echo -n "$key" | tr -d '\n'
  100. echo " MonkeySphere${DATE}"
  101. }
  102. # output authorized_keys line from ssh key
  103. ssh2authorized_keys() {
  104. local userID
  105. local key
  106. userID="$1"
  107. key="$2"
  108. echo -n "$key" | tr -d '\n'
  109. echo " MonkeySphere${DATE} ${userID}"
  110. }
  111. # convert key from gpg to ssh known_hosts format
  112. gpg2known_hosts() {
  113. local host
  114. local keyID
  115. host="$1"
  116. keyID="$2"
  117. # NOTE: it seems that ssh-keygen -R removes all comment fields from
  118. # all lines in the known_hosts file. why?
  119. # NOTE: just in case, the COMMENT can be matched with the
  120. # following regexp:
  121. # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
  122. echo -n "$host "
  123. gpg2ssh "$keyID" | tr -d '\n'
  124. echo " MonkeySphere${DATE}"
  125. }
  126. # convert key from gpg to ssh authorized_keys format
  127. gpg2authorized_keys() {
  128. local userID
  129. local keyID
  130. userID="$1"
  131. keyID="$2"
  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. gpg2ssh "$keyID" | tr -d '\n'
  136. echo " MonkeySphere${DATE} ${userID}"
  137. }
  138. ### GPG UTILITIES
  139. # retrieve all keys with given user id from keyserver
  140. # FIXME: need to figure out how to retrieve all matching keys
  141. # (not just first N (5 in this case))
  142. gpg_fetch_userid() {
  143. local userID
  144. userID="$1"
  145. log -n " checking keyserver $KEYSERVER... "
  146. echo 1,2,3,4,5 | \
  147. gpg --quiet --batch --with-colons \
  148. --command-fd 0 --keyserver "$KEYSERVER" \
  149. --search ="$userID" > /dev/null 2>&1
  150. loge "done."
  151. }
  152. # get the full fingerprint of a key ID
  153. get_key_fingerprint() {
  154. local keyID
  155. keyID="$1"
  156. gpg --list-key --with-colons --fixed-list-mode \
  157. --with-fingerprint "$keyID" | grep "$keyID" | \
  158. grep '^fpr:' | cut -d: -f10
  159. }
  160. ########################################################################
  161. ### PROCESSING FUNCTIONS
  162. # userid and key policy checking
  163. # the following checks policy on the returned keys
  164. # - checks that full key has appropriate valididy (u|f)
  165. # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
  166. # - checks that requested user ID has appropriate validity
  167. # (see /usr/share/doc/gnupg/DETAILS.gz)
  168. # output is one line for every found key, in the following format:
  169. #
  170. # flag fingerprint
  171. #
  172. # "flag" is an acceptability flag, 0 = ok, 1 = bad
  173. # "fingerprint" is the fingerprint of the key
  174. #
  175. # expects global variable: "MODE"
  176. process_user_id() {
  177. local userID
  178. local requiredCapability
  179. local requiredPubCapability
  180. local gpgOut
  181. local type
  182. local validity
  183. local keyid
  184. local uidfpr
  185. local usage
  186. local keyOK
  187. local uidOK
  188. local lastKey
  189. local lastKeyOK
  190. local fingerprint
  191. userID="$1"
  192. # set the required key capability based on the mode
  193. if [ "$MODE" = 'known_hosts' ] ; then
  194. requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
  195. elif [ "$MODE" = 'authorized_keys' ] ; then
  196. requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
  197. fi
  198. requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
  199. # if CHECK_KEYSERVER variable set, check the keyserver
  200. # for the user ID
  201. if [ "$CHECK_KEYSERVER" = "true" ] ; then
  202. gpg_fetch_userid "$userID"
  203. fi
  204. # output gpg info for (exact) userid and store
  205. gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
  206. --with-fingerprint --with-fingerprint \
  207. ="$userID" 2>/dev/null)
  208. # if the gpg query return code is not 0, return 1
  209. if [ "$?" -ne 0 ] ; then
  210. log " - key not found."
  211. return 1
  212. fi
  213. # loop over all lines in the gpg output and process.
  214. # need to do it this way (as opposed to "while read...") so that
  215. # variables set in loop will be visible outside of loop
  216. echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
  217. while IFS=: read -r type validity keyid uidfpr usage ; do
  218. # process based on record type
  219. case $type in
  220. 'pub') # primary keys
  221. # new key, wipe the slate
  222. keyOK=
  223. uidOK=
  224. lastKey=pub
  225. lastKeyOK=
  226. fingerprint=
  227. log " primary key found: $keyid"
  228. # if overall key is not valid, skip
  229. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  230. log " - unacceptable primary key validity ($validity)."
  231. continue
  232. fi
  233. # if overall key is disabled, skip
  234. if check_capability "$usage" 'D' ; then
  235. log " - key disabled."
  236. continue
  237. fi
  238. # if overall key capability is not ok, skip
  239. if ! check_capability "$usage" $requiredPubCapability ; then
  240. log " - unacceptable primary key capability ($usage)."
  241. continue
  242. fi
  243. # mark overall key as ok
  244. keyOK=true
  245. # mark primary key as ok if capability is ok
  246. if check_capability "$usage" $requiredCapability ; then
  247. lastKeyOK=true
  248. fi
  249. ;;
  250. 'uid') # user ids
  251. # if an acceptable user ID was already found, skip
  252. if [ "$uidOK" ] ; then
  253. continue
  254. fi
  255. # if the user ID does not match, skip
  256. if [ "$(unescape "$uidfpr")" != "$userID" ] ; then
  257. continue
  258. fi
  259. # if the user ID validity is not ok, skip
  260. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  261. continue
  262. fi
  263. # mark user ID acceptable
  264. uidOK=true
  265. # output a line for the primary key
  266. # 0 = ok, 1 = bad
  267. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  268. log " * acceptable key found."
  269. echo 0 "$fingerprint"
  270. else
  271. echo 1 "$fingerprint"
  272. fi
  273. ;;
  274. 'sub') # sub keys
  275. # unset acceptability of last key
  276. lastKey=sub
  277. lastKeyOK=
  278. fingerprint=
  279. # if sub key validity is not ok, skip
  280. if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
  281. continue
  282. fi
  283. # if sub key capability is not ok, skip
  284. if ! check_capability "$usage" $requiredCapability ; then
  285. continue
  286. fi
  287. # mark sub key as ok
  288. lastKeyOK=true
  289. ;;
  290. 'fpr') # key fingerprint
  291. fingerprint="$uidfpr"
  292. # if the last key was the pub key, skip
  293. if [ "$lastKey" = pub ] ; then
  294. continue
  295. fi
  296. # output a line for the last subkey
  297. # 0 = ok, 1 = bad
  298. if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
  299. log " * acceptable key found."
  300. echo 0 "$fingerprint"
  301. else
  302. echo 1 "$fingerprint"
  303. fi
  304. ;;
  305. esac
  306. done
  307. }
  308. # process a host in known_host file
  309. process_host_known_hosts() {
  310. local host
  311. local userID
  312. local ok
  313. local keyid
  314. local tmpfile
  315. host="$1"
  316. userID="ssh://${host}"
  317. log "processing host: $host"
  318. process_user_id "ssh://${host}" | \
  319. while read -r ok keyid ; do
  320. sshKey=$(gpg2ssh "$keyid")
  321. # remove the old host key line
  322. remove_line "$KNOWN_HOSTS" "$sshKey"
  323. # if key OK, add new host line
  324. if [ "$ok" -eq '0' ] ; then
  325. # hash if specified
  326. if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
  327. # FIXME: this is really hackish cause ssh-keygen won't
  328. # hash from stdin to stdout
  329. tmpfile=$(mktemp)
  330. ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
  331. ssh-keygen -H -f "$tmpfile" 2> /dev/null
  332. cat "$tmpfile" >> "$KNOWN_HOSTS"
  333. rm -f "$tmpfile" "${tmpfile}.old"
  334. else
  335. ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
  336. fi
  337. fi
  338. done
  339. }
  340. # process a uid in an authorized_keys file
  341. process_uid_authorized_keys() {
  342. local userID
  343. local ok
  344. local keyid
  345. userID="$1"
  346. log "processing user ID: $userID"
  347. process_user_id "$userID" | \
  348. while read -r ok keyid ; do
  349. sshKey=$(gpg2ssh "$keyid")
  350. # remove the old host key line
  351. remove_line "$AUTHORIZED_KEYS" "$sshKey"
  352. # if key OK, add new host line
  353. if [ "$ok" -eq '0' ] ; then
  354. ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
  355. fi
  356. done
  357. }
  358. # process known_hosts file
  359. # go through line-by-line, extract each host, and process with the
  360. # host processing function
  361. process_known_hosts() {
  362. local hosts
  363. local host
  364. # take all the hosts from the known_hosts file (first field),
  365. # grep out all the hashed hosts (lines starting with '|')...
  366. cat "$KNOWN_HOSTS" | meat | \
  367. cut -d ' ' -f 1 | grep -v '^|.*$' | \
  368. while IFS=, read -r -a hosts ; do
  369. # and process each host
  370. for host in ${hosts[*]} ; do
  371. process_host_known_hosts "$host"
  372. done
  373. done
  374. }
  375. # process an authorized_user_ids file for authorized_keys
  376. process_authorized_user_ids() {
  377. local userid
  378. authorizedUserIDs="$1"
  379. cat "$authorizedUserIDs" | meat | \
  380. while read -r userid ; do
  381. process_uid_authorized_keys "$userid"
  382. done
  383. }
  384. # EXPERIMENTAL (unused) process userids found in authorized_keys file
  385. # go through line-by-line, extract monkeysphere userids from comment
  386. # fields, and process each userid
  387. # NOT WORKING
  388. process_authorized_keys() {
  389. local authorizedKeys
  390. local userID
  391. authorizedKeys="$1"
  392. # take all the monkeysphere userids from the authorized_keys file
  393. # comment field (third field) that starts with "MonkeySphere uid:"
  394. # FIXME: needs to handle authorized_keys options (field 0)
  395. cat "$authorizedKeys" | meat | \
  396. while read -r options keytype key comment ; do
  397. # if the comment field is empty, assume the third field was
  398. # the comment
  399. if [ -z "$comment" ] ; then
  400. comment="$key"
  401. fi
  402. if echo "$comment" | egrep -v -q '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}' ; then
  403. continue
  404. fi
  405. userID=$(echo "$comment" | awk "{ print $2 }")
  406. if [ -z "$userID" ] ; then
  407. continue
  408. fi
  409. # process the userid
  410. log "processing userid: '$userID'"
  411. process_user_id "$userID" > /dev/null
  412. done
  413. }
  414. ##################################################
  415. ### GPG HELPER FUNCTIONS
  416. # retrieve key from web of trust, and set owner trust to "full"
  417. # if key is found.
  418. trust_key() {
  419. # get the key from the key server
  420. if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then
  421. log "could not retrieve key '$keyID'"
  422. return 1
  423. fi
  424. # get key fingerprint
  425. fingerprint=$(get_key_fingerprint "$keyID")
  426. # attach a "non-exportable" signature to the key
  427. # this is required for the key to have any validity at all
  428. # the 'y's on stdin indicates "yes, i really want to sign"
  429. echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint"
  430. # import "full" trust for fingerprint into gpg
  431. echo ${fingerprint}:5: | gpg --import-ownertrust
  432. if [ $? = 0 ] ; then
  433. log "owner trust updated."
  434. else
  435. failure "there was a problem changing owner trust."
  436. fi
  437. }
  438. # publish server key to keyserver
  439. publish_server_key() {
  440. read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N}
  441. if [ ${OK/y/Y} != 'Y' ] ; then
  442. failure "aborting."
  443. fi
  444. # publish host key
  445. # FIXME: need to figure out better way to identify host key
  446. # dummy command so as not to publish fakes keys during testing
  447. # eventually:
  448. #gpg --keyserver "$KEYSERVER" --send-keys $(hostname -f)
  449. echo "NOT PUBLISHED (to avoid permanent publication errors during monkeysphere development).
  450. To publish manually, do: gpg --keyserver $KEYSERVER --send-keys $(hostname -f)"
  451. return 1
  452. }