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