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