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