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