# caff -- CA - Fire and Forget
# $Id$
#
-# Copyright (c) 2004, 2005 Peter Palfrader <peter@palfrader.org>
-# Copyright (c) 2005 Christoph Berg <cb@df7cb.de>
+# Copyright (c) 2004, 2005, 2006 Peter Palfrader <peter@palfrader.org>
+# Copyright (c) 2005, 2006 Christoph Berg <cb@df7cb.de>
#
# All rights reserved.
#
=over
-=item B<caff> [-eEmMRS] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
+=item B<caff> [-eERS] [-m I<yes|ask-yes|ask-no|no>] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
=back
Do not export old signatures. Default is to ask the user for each old
signature.
-=item B<-m>, B<--mail>
+=item B<-m>, B<--mail> I<yes|ask-yes|ask-no|no>
-Send mail after signing. Default is to ask the user for each uid.
-
-=item B<-M>, B<--no-mail>
-
-Do not send mail after signing. Default is to ask the user for each uid.
+Whether to send mail after signing. Default is to ask, for each uid,
+with a default value of yes.
=item B<-R>, B<--no-download>
=item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
Select the key that is used for signing, in case you have more than one key.
+To sign with multiple keys at once, separate multiple keyids by comma.
=item B<--key-file> I<file>
=item $HOME/.caff/gnupghome/gpg.conf - gpg configuration
-useful options include use-agent, default-cert-level, etc.
+useful options include use-agent, keyserver-options, default-cert-level, etc.
=back
Example:
- $CONFIG{owner} = q{Peter Palfrader};
- $CONFIG{email} = q{peter@palfrader.org};
- $CONFIG{keyid} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
+ $CONFIG{'owner'} = q{Peter Palfrader};
+ $CONFIG{'email'} = q{peter@palfrader.org};
+ $CONFIG{'keyid'} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
=head2 Required basic settings
Path to your secret keyring. Default: B<$HOME/.gnupg/secring.gpg>.
-=item B<also-encrypt-to> [keyid]
+=item B<also-encrypt-to> [keyid, or list of keyids]
-An additional keyid to encrypt messages to. Default: none.
+Additional keyids to encrypt messages to. Default: none.
=item B<gpg-sign-args> [string]
-Additional arguments to pass to gpg. Default: none.
+Additional commands to pass to gpg after the "sign" command.
+Default: none.
=head2 Keyserver settings
Don't export UIDs by default, on which your latest signature is older
than this age. Default: B<24*60*60> (i.e. one day).
+=item B<local-user> [keyid, or list of keyids]
+
+Select the key that is used for signing, in case you have more than one key.
+With multiple keyids, sign with each key in turn.
+
=head2 Mail settings
=item B<mail> [boolean]
-Do not prompt for sending mail, just do it. Default: B<0>.
+Whether to send mails. This is a quad-option, with which you can set the
+behaviour: yes always sends, no never sends; ask-yes and ask-no asks, for
+each uid, with according defaults for the question. Default: B<ask-yes>.
-=item B<no-mail> [boolean]
-
-Do not prompt for sending mail. The messages are still written to
-$CONFIG{caffhome}/keys/. Default: B<0>.
+In any case, the messages are also written to $CONFIG{'caffhome'}/keys/
=item B<mail-template> [string]
Parameters to pass to Mail::Mailer.
This could for example be
- $CONFIG{mailer-send} = [ 'smtp', Server => 'mail.server', Auth => ['user', 'pass'] ]
+ $CONFIG{'mailer-send'} = [ 'smtp', Server => 'mail.server', Auth => ['user', 'pass'] ];
to use the perl SMTP client or
- $CONFIG{mailer-send} = [ 'sendmail', '-o8' ]
+ $CONFIG{'mailer-send'} = [ 'sendmail', '-o8' ];
to pass arguments to the sendmail program.
For more information run C<< perldoc Mail::Mailer >>.
use File::Temp qw{tempdir};
use Text::Template;
use MIME::Entity;
+use Encode;
use Fcntl;
use IO::Select;
use Getopt::Long;
my $gecos = defined $ENV{'LOGNAME'} ? (getpwnam($ENV{LOGNAME}))[6] : undef;
my $email;
my @keys;
- my $hostname = `hostname -f`;
+ # BSD does not have hostname -f, so we try without -f first
+ my $hostname = `hostname`;
+ $hostname = `hostname -f` unless $hostname =~ /\./;
chomp $hostname;
my ($Cgecos,$Cemail,$Ckeys) = ('','','');
+
if (defined $gecos) {
$gecos =~ s/,.*//;
($Cgecos,$Cemail,$Ckeys) = ('#','#','#');
};
- return <<EOT;
-# .caffrc -- vim:syntax=perl:
+ my $template = <<EOT;
+# .caffrc -- vim:ft=perl:
# This file is in perl(1) format - see caff(1) for details.
-$Cgecos\$CONFIG{'owner'} = '$gecos';
-$Cemail\$CONFIG{'email'} = '$email';
+$Cgecos\$CONFIG{'owner'} = '$gecos';
+$Cemail\$CONFIG{'email'} = '$email';
+#\$CONFIG{'reply-to'} = 'foo\@bla.org';
-# you can get your long keyid from
+# You can get your long keyid from
# gpg --with-colons --list-key <yourkeyid|name|emailaddress..>
#
-# if you have a v4 key, it will simply be the last 16 digits of
+# If you have a v4 key, it will simply be the last 16 digits of
# your fingerprint.
#
# Example:
-# \$CONFIG{'keyid'} = [ qw{FEDCBA9876543210} ];
+# \$CONFIG{'keyid'} = [ qw{FEDCBA9876543210} ];
# or, if you have more than one key:
-# \$CONFIG{'keyid'} = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
+# \$CONFIG{'keyid'} = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
+$Ckeys\$CONFIG{'keyid'} = [ qw{@keys} ];
+
+# Select this/these keys to sign with
+#\$CONFIG{'local-user'} = [ qw{@keys} ];
-$Ckeys\$CONFIG{'keyid'} = [ qw{@keys} ];
+# Additionally encrypt messages for these keyids
+#\$CONFIG{'also-encrypt-to'} = [ qw{@keys} ];
+
+# Mail template to use for the encrypted part
+#\$CONFIG{'mail-template'} = << 'EOM';
EOT
+
+ $template .= "#$_" foreach <DATA>;
+ $template .= "#EOM\n";
+ return $template;
};
sub check_executable($$) {
check_executable("gpg", $CONFIG{'gpg'});
check_executable("gpg-sign", $CONFIG{'gpg-sign'});
check_executable("gpg-delsig", $CONFIG{'gpg-delsig'});
- $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
+ $CONFIG{'secret-keyring'} = ($ENV{'GNUPGHOME'} || "$ENV{'HOME'}/.gnupg") . '/secring.gpg'
+ unless defined $CONFIG{'secret-keyring'};
$CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
$CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
$CONFIG{'key-files'} = () unless defined $CONFIG{'key-files'};
$CONFIG{'mailer-send'} = [] unless defined $CONFIG{'mailer-send'};
die ("$PROGRAM_NAME: mailer-send is not an array ref in $config.\n") unless (ref $CONFIG{'mailer-send'} eq 'ARRAY');
- $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
-Hi,
-
-please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
-{foreach $uid (@uids) {
- $OUT .= "\t".$uid."\n";
-};} of your key {$key} signed by me.
-
-Note that I did not upload your key to any keyservers.
-If you have multiple user ids, I sent the signature for each user id
-separately to that user id's associated email address. You can import
-the signatures by running each through `gpg --import`.
-
-If you want this new signature to be available to others, please upload
-it yourself. With GnuPG this can be done using
- gpg --keyserver subkeys.pgp.net --send-key {$key}
-
-If you have any questions, don't hesitate to ask.
-
-Regards,
-{$owner}
-EOM
+ unless (defined $CONFIG{'mail-template'}) {
+ $CONFIG{'mail-template'} .= $_ foreach <DATA>;
+ }
+ if ((defined $CONFIG{'also-encrypt-to'}) && ! (ref($CONFIG{'also-encrypt-to'}) eq 'ARRAY')) {
+ $CONFIG{'also-encrypt-to'} = [ $CONFIG{'also-encrypt-to'} ];
+ };
};
sub make_gpg_fds() {
my $KEYEDIT_DELSUBKEY_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.subkey';
load_config;
-my $USER_AGENT = "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.";
+my $USER_AGENT = "caff $VERSION - http://pgp-tools.alioth.debian.org/";
my $KEYSBASE = $CONFIG{'caffhome'}.'/keys';
my $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
sub version($) {
my ($fd) = @_;
- print $fd "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.\n";
+ print $fd "caff $VERSION - (c) 2004, 2005, 2006 Peter Palfrader et al.\n";
};
sub usage($$) {
my ($fd, $exitcode) = @_;
version($fd);
- print $fd "Usage: $PROGRAM_NAME [-eEmMRS] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
+ print $fd "Usage: $PROGRAM_NAME [-eERS] [-m <yes|ask-yes|ask-no|no>] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
print $fd "Consult the manual page for more information.\n";
exit $exitcode;
};
$gpg->options->meta_interactive( 0 );
my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
$gpg->options->push_recipients( $key_id );
- $gpg->options->push_recipients( $CONFIG{'also-encrypt-to'} ) if defined $CONFIG{'also-encrypt-to'};
+ if (defined $CONFIG{'also-encrypt-to'}) {
+ $gpg->options->push_recipients($_) foreach @{$CONFIG{'also-encrypt-to'}};
+ }
my $pid = $gpg->encrypt(handles => $handles);
my ($stdout, $stderr, $status) = readwrite_gpg($message, $inputfd, $stdoutfd, $stderrfd, $statusfd);
waitpid $pid, 0;
if ($stdout eq '') {
- warn ("No data from gpg for list-key $key_id\n");
- next;
+ if (($status =~ /^\[GNUPG:\] INV_RECP ([0-9]+) ([0-9A-F]+)/m) and
+ (defined $CONFIG{'also-encrypt-to'})) {
+ my $reason = $1;
+ my $keyid = $2;
+ if (grep { $_ eq $keyid } @{$CONFIG{'also-encrypt-to'}}) {
+ warn("Could not encrypt to $keyid, specified in CONFIG{'also-encrypt-to'}.\n".
+ "Try to update the key using gpg --homedir=$GNUPGHOME --import <exported key>\n".
+ "or try the following if you are slightly more daring:\n".
+ " gpg --export $keyid | gpg --homedir=$GNUPGHOME --import\n");
+ return;
+ };
+ };
+ warn ("No data from gpg for encrypting mail. STDERR was:\n$stderr\nstatus output was:\n$status\n");
+ return;
};
$message = $stdout;
$message_entity->attach(
Type => "application/pgp-encrypted",
+ Filename => "signedkey.msg",
Disposition => 'attachment',
Encoding => "7bit",
Data => "Version: 1\n");
$message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
$message_entity->head->add("To", $address);
- $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
+ $message_entity->head->add("From", '"'.Encode::encode('MIME-Q', $CONFIG{'owner'}).'" <'.$CONFIG{'email'}.'>');
$message_entity->head->add("Reply-To", $CONFIG{'reply-to'}) if defined $CONFIG{'reply-to'};
$message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
$message_entity->head->add("User-Agent", $USER_AGENT);
};
-
-my $USER;
+###################
+# argument handling
+###################
my @KEYIDS;
my $params;
'--export-old' => \$params->{'export-old'},
'-E' => \$params->{'no-export-old'},
'--no-export-old' => \$params->{'no-export-old'},
- '-m' => \$params->{'mail'},
- '--mail' => \$params->{'mail'},
+ '-m:s' => \$params->{'mail'},
+ '--mail:s' => \$params->{'mail'},
'-M' => \$params->{'no-mail'},
'--no-mail' => \$params->{'no-mail'},
'-R' => \$params->{'no-download'},
};
usage(\*STDERR, 1) unless scalar @ARGV >= 1;
+$CONFIG{'local-user'} = $params->{'local-user'} if defined $params->{'local-user'};
+$CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
+$CONFIG{'no-sign'} = $params->{'no-sign'} if defined $params->{'no-sign'};
+# If old 'no-mail' parameter, or if the 'mail' parameter is set to 'no'
+if ( $CONFIG{'no-mail'} || defined $params->{'no-mail'} ||
+ ( defined $params->{'mail'} && $params->{'mail'} eq 'no' ) ) {
+ $CONFIG{'mail'} = 'no';
+} elsif ( defined $params->{'mail'} ) {
+ $CONFIG{'mail'} = $params->{'mail'};
+} else {
+ $CONFIG{'mail'} = 'ask-yes';
+}
-if ($params->{'local-user'}) {
- $USER = $params->{'local-user'};
- $USER =~ s/^0x//i;
- unless ($USER =~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
- print STDERR "-u $USER is not a keyid.\n";
- usage(\*STDERR, 1);
- };
- $USER = uc($USER);
-};
+$CONFIG{'no-mail'} = $params->{'no-mail'} if defined $params->{'no-mail'};
+$CONFIG{'mail'} = $params->{'mail'} if defined $params->{'mail'};
+
+push @{$CONFIG{'key-files'}}, @{$params->{'key-files'}} if defined $params->{'key-files'};
-for my $keyid (@ARGV) {
+for my $keyid (map { split /\n/ } @ARGV) { # caff "`cat txt`" is a single argument
$keyid =~ s/^0x//i;
- unless ($keyid =~ /^([A-F0-9]{8}|[A-F0-9]{16}||[A-F0-9]{40})$/i) {
- if ($keyid =~ /^[A-F0-9]{32}$/) {
- info("Ignoring v3 fingerprint $keyid. v3 keys are obsolete.");
- next;
- };
+ $keyid =~ s/ //g; # gpg --fingerprint includes spaces
+ if ($keyid =~ /^[A-F0-9]{32}$/i) {
+ info("Ignoring v3 fingerprint $keyid. v3 keys are obsolete.");
+ next;
+ };
+ if ($keyid !~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
print STDERR "$keyid is not a keyid.\n";
usage(\*STDERR, 1);
};
push @KEYIDS, uc($keyid);
};
-$CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
-$CONFIG{'no-mail'} = $params->{'no-mail'} if defined $params->{'no-mail'};
-$CONFIG{'mail'} = $params->{'mail'} if defined $params->{'mail'};
-$CONFIG{'no-sign'} = $params->{'no-sign'} if defined $params->{'no-sign'};
-push @{$CONFIG{'key-files'}}, @{$params->{'key-files'}} if defined $params->{'key-files'};
-
-
#################
# import own keys
#################
if (scalar %local_keyids) {
notice ("Import failed for: ". (join ' ', keys %local_keyids)."." . ($had_v3_keys ? " (Or maybe it's one of those ugly v3 keys?)" : ""));
exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
- if (scalar %local_keyids == 1) {
+ if (scalar keys %local_keyids == 1) {
mywarn("Assuming ". (join ' ', keys %local_keyids)." is a fine keyid.");
} else {
mywarn("Assuming ". (join ' ', keys %local_keyids)." are fine keyids.");
}
unless ($CONFIG{'no-sign'}) {
+ my @local_user;
+ if ($CONFIG{'local-user'}) {
+ if (ref($CONFIG{'local-user'})) {
+ @local_user = @{$CONFIG{'local-user'}};
+ } else {
+ @local_user = split /\s*,\s*/, $CONFIG{'local-user'};
+ };
+ foreach (@local_user) {
+ s/^0x//i;
+ unless (/^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
+ print STDERR "Local-user $_ is not a keyid.\n";
+ usage(\*STDERR, 1);
+ };
+ $_ = uc($_);
+ };
+ } else {
+ @local_user = (undef);
+ };
+
info("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
for my $keyid (@keyids_ok) {
- my @command;
- push @command, $CONFIG{'gpg-sign'};
- push @command, '--local-user', $USER if (defined $USER);
- push @command, "--homedir=$GNUPGHOME";
- push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
- push @command, '--no-auto-check-trustdb';
- push @command, '--trust-model=always';
- push @command, '--edit', $keyid;
- push @command, 'sign';
- push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
- print join(' ', @command),"\n";
- system (@command);
+ foreach my $local_user (@local_user) {
+ my @command;
+ push @command, $CONFIG{'gpg-sign'};
+ push @command, '--local-user', $local_user if (defined $local_user);
+ push @command, "--homedir=$GNUPGHOME";
+ push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
+ push @command, '--no-auto-check-trustdb';
+ push @command, '--trust-model=always';
+ push @command, '--edit', $keyid;
+ push @command, 'sign';
+ push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
+ print join(' ', @command),"\n";
+ system (@command);
+ };
};
};
if (scalar @UIDS == 0) {
info("found no signed uids for $keyid");
} else {
- next if $CONFIG{'no-mail'}; # do not send mail
+ next if ($CONFIG{'mail'} eq 'no'); # do not send mail
my @attached;
for my $uid (@UIDS) {
if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
my $address = $uid->{'text'};
$address =~ s/.*<(.*)>.*/$1/;
- if (ask("Mail signature for $uid->{'text'} to '$address'?", 1, $CONFIG{'mail'})) {
+ if (ask("Mail signature for $uid->{'text'} to '$address'?", $CONFIG{'mail'} ne 'ask-no', $CONFIG{'mail'} eq 'yes')) {
my $mail = send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
-
- my $keydir = "$KEYSBASE/$DATE_STRING";
- my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid($uid->{'text'});
- open (KEY, ">$mailfile") or die ("Cannot open $mailfile: $!\n");
- print KEY $mail;
- close KEY;
+ if (defined $mail) {
+ my $keydir = "$KEYSBASE/$DATE_STRING";
+ my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid($uid->{'text'});
+ open (KEY, ">$mailfile") or die ("Cannot open $mailfile: $!\n");
+ print KEY $mail;
+ close KEY;
+ } else {
+ warn "Generating mail failed.\n";
+ };
};
};
};
};
};
+
+###########################
+# the default mail template
+###########################
+
+__DATA__
+Hi,
+
+please find attached the user id{(scalar @uids >= 2 ? 's' : '')}
+{foreach $uid (@uids) {
+ $OUT .= "\t".$uid."\n";
+};}of your key {$key} signed by me.
+
+If you have multiple user ids, I sent the signature for each user id
+separately to that user id's associated email address. You can import
+the signatures by running each through `gpg --import`.
+
+Note that I did not upload your key to any keyservers. If you want this
+new signature to be available to others, please upload it yourself.
+With GnuPG this can be done using
+ gpg --keyserver subkeys.pgp.net --send-key {$key}
+
+If you have any questions, don't hesitate to ask.
+
+Regards,
+{$owner}