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