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