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