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