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