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