summaryrefslogtreecommitdiff
path: root/src/monkeysphere
blob: 46abf6fce659dcc09f7017ac97e2a9c814a85105 (plain)
  1. #!/usr/bin/env bash
  2. # monkeysphere: MonkeySphere client tool
  3. #
  4. # The monkeysphere scripts are written by:
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. # Jamie McClelland <jm@mayfirst.org>
  7. # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
  8. # Micah Anderson <micah@riseup.net>
  9. #
  10. # They are Copyright 2008-2009, and are all released under the GPL, version 3
  11. # or later.
  12. ########################################################################
  13. PGRM=$(basename $0)
  14. SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
  15. export SYSSHAREDIR
  16. . "${SYSSHAREDIR}/common" || exit 1
  17. # UTC date in ISO 8601 format if needed
  18. DATE=$(date -u '+%FT%T')
  19. # unset some environment variables that could screw things up
  20. unset GREP_OPTIONS
  21. # default return code
  22. RETURN=0
  23. # set the file creation mask to be only owner rw
  24. umask 077
  25. ########################################################################
  26. # FUNCTIONS
  27. ########################################################################
  28. usage() {
  29. cat <<EOF >&2
  30. usage: $PGRM <subcommand> [options] [args]
  31. Monkeysphere client tool.
  32. subcommands:
  33. update-known_hosts (k) [HOST]... update known_hosts file
  34. update-authorized_keys (a) update authorized_keys file
  35. import-subkey (i) import existing ssh key as gpg subkey
  36. --keyfile (-f) FILE key file to import
  37. --expire (-e) EXPIRE date to expire
  38. gen-subkey (g) [KEYID] generate an authentication subkey
  39. --length (-l) BITS key length in bits (2048)
  40. --expire (-e) EXPIRE date to expire
  41. ssh-proxycommand ssh proxycommand
  42. subkey-to-ssh-agent (s) store authentication subkey in ssh-agent
  43. version (v) show version number
  44. help (h,?) this help
  45. EOF
  46. }
  47. # import an existing ssh key as a gpg subkey
  48. import_subkey() {
  49. local keyFile="~/.ssh/id_rsa"
  50. local keyExpire
  51. local keyID
  52. local gpgOut
  53. local userID
  54. # get options
  55. while true ; do
  56. case "$1" in
  57. -f|--keyfile)
  58. keyFile="$2"
  59. shift 2
  60. ;;
  61. -e|--expire)
  62. keyExpire="$2"
  63. shift 2
  64. ;;
  65. *)
  66. if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
  67. failure "Unknown option '$1'.
  68. Type '$PGRM help' for usage."
  69. fi
  70. break
  71. ;;
  72. esac
  73. done
  74. log verbose "importing ssh key..."
  75. fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
  76. (umask 077 && mkfifo "$fifoDir/pass")
  77. ssh2openpgp | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --import &
  78. passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass"
  79. rm -rf "$fifoDir"
  80. wait
  81. log verbose "done."
  82. }
  83. # generate a subkey with the 'a' usage flags set
  84. gen_subkey(){
  85. local keyLength
  86. local keyExpire
  87. local keyID
  88. local gpgOut
  89. local userID
  90. # get options
  91. while true ; do
  92. case "$1" in
  93. -l|--length)
  94. keyLength="$2"
  95. shift 2
  96. ;;
  97. -e|--expire)
  98. keyExpire="$2"
  99. shift 2
  100. ;;
  101. *)
  102. if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
  103. failure "Unknown option '$1'.
  104. Type '$PGRM help' for usage."
  105. fi
  106. break
  107. ;;
  108. esac
  109. done
  110. case "$#" in
  111. 0)
  112. gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:')
  113. ;;
  114. 1)
  115. gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure
  116. ;;
  117. *)
  118. failure "You must specify only a single primary key ID."
  119. ;;
  120. esac
  121. # check that only a single secret key was found
  122. case $(echo "$gpgSecOut" | grep -c '^sec:') in
  123. 0)
  124. failure "No secret keys found. Create an OpenPGP key with the following command:
  125. gpg --gen-key"
  126. ;;
  127. 1)
  128. keyID=$(echo "$gpgSecOut" | cut -d: -f5)
  129. ;;
  130. *)
  131. echo "Multiple primary secret keys found:"
  132. echo "$gpgSecOut" | cut -d: -f5
  133. failure "Please specify which primary key to use."
  134. ;;
  135. esac
  136. # check that a valid authentication key does not already exist
  137. IFS=$'\n'
  138. for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do
  139. type=$(echo "$line" | cut -d: -f1)
  140. validity=$(echo "$line" | cut -d: -f2)
  141. usage=$(echo "$line" | cut -d: -f12)
  142. # look at keys only
  143. if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then
  144. continue
  145. fi
  146. # check for authentication capability
  147. if ! check_capability "$usage" 'a' ; then
  148. continue
  149. fi
  150. # if authentication key is valid, prompt to continue
  151. if [ "$validity" = 'u' ] ; then
  152. echo "A valid authentication key already exists for primary key '$keyID'."
  153. read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
  154. if [ "${OK/y/Y}" != 'Y' ] ; then
  155. failure "aborting."
  156. fi
  157. break
  158. fi
  159. done
  160. # set subkey defaults
  161. # prompt about key expiration if not specified
  162. keyExpire=$(get_gpg_expiration "$keyExpire")
  163. # generate the list of commands that will be passed to edit-key
  164. editCommands=$(cat <<EOF
  165. addkey
  166. 7
  167. S
  168. E
  169. A
  170. Q
  171. $keyLength
  172. $keyExpire
  173. save
  174. EOF
  175. )
  176. log verbose "generating subkey..."
  177. fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
  178. (umask 077 && mkfifo "$fifoDir/pass")
  179. echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
  180. # FIXME: this needs to fail more gracefully if the passphrase is incorrect
  181. passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass"
  182. rm -rf "$fifoDir"
  183. wait
  184. log verbose "done."
  185. }
  186. subkey_to_ssh_agent() {
  187. # try to add all authentication subkeys to the agent:
  188. local sshaddresponse
  189. local secretkeys
  190. local authsubkeys
  191. local workingdir
  192. local keysuccess
  193. local subkey
  194. local publine
  195. local kname
  196. if ! test_gnu_dummy_s2k_extension ; then
  197. failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
  198. You may want to consider patching or upgrading to GnuTLS 2.6 or later.
  199. For more details, see:
  200. http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
  201. fi
  202. # if there's no agent running, don't bother:
  203. if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
  204. failure "No ssh-agent available."
  205. fi
  206. # and if it looks like it's running, but we can't actually talk to
  207. # it, bail out:
  208. ssh-add -l >/dev/null
  209. sshaddresponse="$?"
  210. if [ "$sshaddresponse" = "2" ]; then
  211. failure "Could not connect to ssh-agent"
  212. fi
  213. # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
  214. secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
  215. grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
  216. if [ -z "$secretkeys" ]; then
  217. failure "You have no secret keys in your keyring!
  218. You might want to run 'gpg --gen-key'."
  219. fi
  220. authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
  221. --fingerprint --fingerprint $secretkeys | \
  222. cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
  223. grep '^fpr::' | cut -f3 -d: | sort -u)
  224. if [ -z "$authsubkeys" ]; then
  225. failure "no authentication-capable subkeys available.
  226. You might want to 'monkeysphere gen-subkey'"
  227. fi
  228. workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
  229. umask 077
  230. mkfifo "$workingdir/passphrase"
  231. keysuccess=1
  232. # FIXME: we're currently allowing any other options to get passed
  233. # through to ssh-add. should we limit it to known ones? For
  234. # example: -d or -c and/or -t <lifetime>
  235. for subkey in $authsubkeys; do
  236. # choose a label by which this key will be known in the agent:
  237. # we are labelling the key by User ID instead of by
  238. # fingerprint, but filtering out all / characters to make sure
  239. # the filename is legit.
  240. primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
  241. #kname="[monkeysphere] $primaryuid"
  242. kname="$primaryuid"
  243. if [ "$1" = '-d' ]; then
  244. # we're removing the subkey:
  245. gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
  246. (cd "$workingdir" && ssh-add -d "$kname")
  247. else
  248. # we're adding the subkey:
  249. mkfifo "$workingdir/$kname"
  250. gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
  251. --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
  252. --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
  253. (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
  254. passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
  255. wait %2
  256. fi
  257. keysuccess="$?"
  258. rm -f "$workingdir/$kname"
  259. done
  260. rm -rf "$workingdir"
  261. # FIXME: sort out the return values: we're just returning the
  262. # success or failure of the final authentication subkey in this
  263. # case. What if earlier ones failed?
  264. exit "$keysuccess"
  265. }
  266. ########################################################################
  267. # MAIN
  268. ########################################################################
  269. # unset variables that should be defined only in config file
  270. unset KEYSERVER
  271. unset CHECK_KEYSERVER
  272. unset KNOWN_HOSTS
  273. unset HASH_KNOWN_HOSTS
  274. unset AUTHORIZED_KEYS
  275. # load global config
  276. [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"
  277. # set monkeysphere home directory
  278. MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
  279. mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
  280. # load local config
  281. [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
  282. # set empty config variables with ones from the environment, or from
  283. # config file, or with defaults
  284. LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
  285. GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
  286. KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
  287. # if keyserver not specified in env or monkeysphere.conf,
  288. # look in gpg.conf
  289. if [ -z "$KEYSERVER" ] ; then
  290. if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
  291. KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
  292. fi
  293. fi
  294. # if it's still not specified, use the default
  295. KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
  296. CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
  297. KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
  298. HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
  299. AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
  300. # other variables not in config file
  301. AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
  302. REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
  303. REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
  304. # export GNUPGHOME and make sure gpg home exists with proper
  305. # permissions
  306. export GNUPGHOME
  307. mkdir -p -m 0700 "$GNUPGHOME"
  308. export LOG_LEVEL
  309. # get subcommand
  310. COMMAND="$1"
  311. [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
  312. shift
  313. case $COMMAND in
  314. 'update-known_hosts'|'update-known-hosts'|'k')
  315. MODE='known_hosts'
  316. # touch the known_hosts file so that the file permission check
  317. # below won't fail upon not finding the file
  318. (umask 0022 && touch "$KNOWN_HOSTS")
  319. # check permissions on the known_hosts file path
  320. check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
  321. # if hosts are specified on the command line, process just
  322. # those hosts
  323. if [ "$1" ] ; then
  324. update_known_hosts "$@"
  325. RETURN="$?"
  326. # otherwise, if no hosts are specified, process every host
  327. # in the user's known_hosts file
  328. else
  329. # exit if the known_hosts file does not exist
  330. if [ ! -e "$KNOWN_HOSTS" ] ; then
  331. log error "known_hosts file '$KNOWN_HOSTS' does not exist."
  332. exit
  333. fi
  334. process_known_hosts
  335. RETURN="$?"
  336. fi
  337. ;;
  338. 'update-authorized_keys'|'update-authorized-keys'|'a')
  339. MODE='authorized_keys'
  340. # check permissions on the authorized_user_ids file path
  341. check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
  342. # check permissions on the authorized_keys file path
  343. check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
  344. # exit if the authorized_user_ids file is empty
  345. if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
  346. log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
  347. exit
  348. fi
  349. # process authorized_user_ids file
  350. process_authorized_user_ids "$AUTHORIZED_USER_IDS"
  351. RETURN="$?"
  352. ;;
  353. 'import-subkey'|'i')
  354. import_key "$@"
  355. ;;
  356. 'gen-subkey'|'g')
  357. gen_subkey "$@"
  358. ;;
  359. 'ssh-proxycommand'|'p')
  360. ssh-proxycommand "$@"
  361. ;;
  362. 'subkey-to-ssh-agent'|'s')
  363. subkey_to_ssh_agent "$@"
  364. ;;
  365. 'version'|'v')
  366. echo "$VERSION"
  367. ;;
  368. '--help'|'help'|'-h'|'h'|'?')
  369. usage
  370. ;;
  371. *)
  372. failure "Unknown command: '$COMMAND'
  373. Type '$PGRM help' for usage."
  374. ;;
  375. esac
  376. exit "$RETURN"