X-Git-Url: https://git.sthu.org/?a=blobdiff_plain;f=caff%2Fcaff;h=8415af93cd7f2b9c083c666412278538c2172c0c;hb=96137d78cc59f874fd894a60fe1b006bfe9aecdf;hp=35be431932aa20e283b390fb3599820552896a9b;hpb=e201631c7543dfd60a38d08c7328bc13f147e149;p=pgp-tools.git diff --git a/caff/caff b/caff/caff index 35be431..8415af9 100755 --- a/caff/caff +++ b/caff/caff @@ -4,7 +4,7 @@ # $Id$ # # Copyright (c) 2004, 2005 Peter Palfrader -# Copyright (c) 2005 Christoph Berg +# Copyright (c) 2005, 2006 Christoph Berg # # All rights reserved. # @@ -50,7 +50,8 @@ CA Fire and Forget is a script that helps you in keysigning. It takes a list of keyids on the command line, fetches them from a keyserver and calls GnuPG so that you can sign it. It then mails each key to all its email addresses - only including the one UID that we send to in each mail, pruned from all but self -sigs and sigs done by you. +sigs and sigs done by you. The mailed key is encrypted with itself as a means +to verify that key belongs to the recipient. =head1 OPTIONS @@ -84,6 +85,7 @@ Do not sign the keys. =item B<-u> I, B<--local-user> I 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 @@ -97,11 +99,20 @@ Import keys from file. Can be supplied more than once. =item $HOME/.caffrc - configuration file +=item $HOME/.caff/keys/yyyy-mm-dd/ - processed keys + +=item $HOME/.caff/gnupghome/ - caff's working dir for gpg + +=item $HOME/.caff/gnupghome/gpg.conf - gpg configuration + +useful options include use-agent, default-cert-level, etc. + =back =head1 CONFIGURATION FILE OPTIONS The configuration file is a perl script that sets values in the hash B<%CONFIG>. +The file is generated when it does not exist. Example: @@ -154,9 +165,9 @@ is set to. Path to your secret keyring. Default: B<$HOME/.gnupg/secring.gpg>. -=item B [keyid] +=item B [keyid, or list of keyids] -An additional keyid to encrypt messages to. Default: none. +Additional keyids to encrypt messages to. Default: none. =item B [string] @@ -193,6 +204,11 @@ This is useful for offline signing. Default: B<0>. 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 [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 [boolean] @@ -226,11 +242,32 @@ The UIDs for which signatures are included in the mail. =back +=item B [string] + +Add a Reply-To: header to messages sent. Default: none. + =item B [string] Address to send blind carbon copies to when sending mail. Default: none. +=item B [array] + +Parameters to pass to Mail::Mailer. +This could for example be + + $CONFIG{mailer-send} = [ 'smtp', Server => 'mail.server', Auth => ['user', 'pass'] ] + +to use the perl SMTP client or + + $CONFIG{mailer-send} = [ 'sendmail', '-o8' ] + +to pass arguments to the sendmail program. +For more information run C<< perldoc Mail::Mailer >>. +Setting this option is strongly discouraged. Fix your local MTA +instead. +Default: none. + =back =head1 AUTHORS @@ -247,6 +284,10 @@ Default: none. http://pgp-tools.alioth.debian.org/ +=head1 SEE ALSO + +gpg(1), pgp-clean(1), /usr/share/doc/signing-party/caff/caffrc.sample. + =cut use strict; @@ -266,49 +307,130 @@ my $REVISION = '$Rev$'; my ($REVISION_NUMER) = $REVISION =~ /(\d+)/; my $VERSION = "0.0.0.$REVISION_NUMER"; -sub generate_config() { - die "Error: \$LOGNAME is not set.\n" unless $ENV{LOGNAME}; - my $gecos = (getpwnam($ENV{LOGNAME}))[6]; - $gecos =~ s/,.*//; - my $gpg = GnuPG::Interface->new(); - $gpg->call( 'gpg' ); - $gpg->options->hash_init( - 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode } ] ); - $gpg->options->meta_interactive( 0 ); - my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds(); - my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $gecos ]); - my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd); - waitpid $pid, 0; - if ($stdout eq '') { - warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere. - }; +sub mywarn($) { + my ($line) = @_; + print "[WARN] $line\n"; +}; +sub notice($) { + my ($line) = @_; + print "[NOTICE] $line\n"; +}; +sub info($) { + my ($line) = @_; + print "[INFO] $line\n"; +}; +sub debug($) { + my ($line) = @_; + #print "[DEBUG] $line\n"; +}; +sub trace($) { + my ($line) = @_; + #print "[trace] $line\n"; +}; +sub trace2($) { + my ($line) = @_; + #print "[trace2] $line\n"; +}; + +sub generate_config() { + notice("Error: \$LOGNAME is not set.\n") unless defined $ENV{'LOGNAME'}; + my $gecos = defined $ENV{'LOGNAME'} ? (getpwnam($ENV{LOGNAME}))[6] : undef; + my $email; my @keys; - unless (@keys = ($stdout =~ /^pub:[^r:]*:(?:[^:]*:){2,2}([^:]+):/mg)) { - die "Error: No keys were found using \"gpg --list-public-keys '$gecos'\".\n"; - } - unless ($stdout =~ /^uid:.*<(.+@.+)>.*:/m) { - die "Error: No email address was found using \"gpg --list-public-keys '$gecos'\".\n"; - } - my $email = $1; + # 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/,.*//; + + my $gpg = GnuPG::Interface->new(); + $gpg->call( 'gpg' ); + $gpg->options->hash_init( + 'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode } ] ); + $gpg->options->meta_interactive( 0 ); + my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds(); + my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $gecos ]); + my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd); + waitpid $pid, 0; - return <.*:/m); + unless (defined $email) { + info("Error: No email address was found using \"gpg --list-public-keys '$gecos'\"."); + $email = $ENV{'LOGNAME'}.'@'.$hostname; + $Cemail = '#'; + } + } else { + $gecos = 'Unknown Caff User'; + $email = $ENV{'LOGNAME'}.'@'.$hostname; + @keys = qw{0123456789abcdef 89abcdef76543210}; + ($Cgecos,$Cemail,$Ckeys) = ('#','#','#'); + }; + + my $template = < # -# 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} ]; +# or, if you have more than one key: +# \$CONFIG{'keyid'} = [ qw{0123456789ABCDEF 89ABCDEF76543210} ]; +$Ckeys\$CONFIG{'keyid'} = [ qw{@keys} ]; + +# Select this/these keys to sign with +#\$CONFIG{'local-user'} = [ qw{@keys} ]; + +# Additionally encrypt messages for these keyids +#\$CONFIG{'also-encrypt-to'} = [ qw{@keys} ]; -\$CONFIG{'keyid'} = [ qw{@keys} ]; +# Mail template to use for the encrypted part +#\$CONFIG{'mail-template'} = << 'EOM'; EOT + + $template .= "#$_" foreach ; + $template .= "#EOM\n"; + return $template; +}; + +sub check_executable($$) { + # (GnuPG::Interface gives lousy errors when the gpg binary isn't found, + # so we want to check manually.) + my ($purpose, $fn) = @_; + # Only check provided fnames with a slash in them. + return unless defined $fn; + if ($fn =~ m!/!) { + die ("$PROGRAM_NAME: $purpose executable '$fn' not found.\n") unless -x $fn; + } else { + for my $p (split(':', $ENV{PATH})) { + return if -x "$p/$fn"; + }; + die ("$PROGRAM_NAME: $purpose executable '$fn' not found on path.\n") unless -x $fn; + }; }; sub load_config() { @@ -316,23 +438,23 @@ sub load_config() { unless (-f $config) { print "No configfile $config present, I will use this template:\n"; my $template = generate_config(); - print "$template\nPress enter to continue."; - ; + print "$template\nPlease edit $config and run caff again.\n"; open F, ">$config" or die "$config: $!"; print F $template; close F; + exit(1); } unless (scalar eval `cat $config`) { die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR; }; $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'}; - die ("owner is not defined.\n") unless defined $CONFIG{'owner'}; - die ("email is not defined.\n") unless defined $CONFIG{'email'}; - die ("keyid is not defined.\n") unless defined $CONFIG{'keyid'}; - die ("keyid is not an array ref\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY'); + die ("$PROGRAM_NAME: owner is not defined in $config.\n") unless defined $CONFIG{'owner'}; + die ("$PROGRAM_NAME: email is not defined in $config.\n") unless defined $CONFIG{'email'}; + die ("$PROGRAM_NAME: keyid is not defined in $config.\n") unless defined $CONFIG{'keyid'}; + die ("$PROGRAM_NAME: keyid is not an array ref in $config.\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY'); for my $keyid (@{$CONFIG{'keyid'}}) { - $keyid =~ /^[A-F0-9]{16}$/i or die ("key $keyid is not a long (16 digit) keyid.\n"); + $keyid =~ /^[A-F0-9]{16}$/i or die ("$PROGRAM_NAME: key $keyid is not a long (16 digit) keyid in $config.\n"); }; @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}}; $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'}; @@ -340,53 +462,18 @@ sub load_config() { $CONFIG{'gpg'} = 'gpg' unless defined $CONFIG{'gpg'}; $CONFIG{'gpg-sign'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-sign'}; $CONFIG{'gpg-delsig'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-delsig'}; + 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{'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{'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 -}; - -sub notice($) { - my ($line) = @_; - print "[NOTICE] $line\n"; -}; -sub info($) { - my ($line) = @_; - print "[INFO] $line\n"; -}; -sub debug($) { - my ($line) = @_; - #print "[DEBUG] $line\n"; -}; -sub trace($) { - my ($line) = @_; - #print "[trace] $line\n"; -}; -sub trace2($) { - my ($line) = @_; - #print "[trace2] $line\n"; + $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'); + unless (defined $CONFIG{'mail-template'}) { + $CONFIG{'mail-template'} .= $_ foreach ; + } }; sub make_gpg_fds() { @@ -548,7 +635,7 @@ my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = '^\[GNUPG:\] (GET_BOOL keyedit.delsig|GET 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'; @@ -563,7 +650,7 @@ my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday); 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($$) { @@ -675,7 +762,14 @@ sub send_mail($$$@) { $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'}) { + if (ref($CONFIG{'also-encrypt-to'})) { + $gpg->options->push_recipients($_) + foreach @{$CONFIG{'also-encrypt-to'}}; + } else { + $gpg->options->push_recipients($CONFIG{'also-encrypt-to'}); + } + } my $pid = $gpg->encrypt(handles => $handles); my ($stdout, $stderr, $status) = readwrite_gpg($message, $inputfd, $stdoutfd, $stderrfd, $statusfd); waitpid $pid, 0; @@ -686,7 +780,8 @@ sub send_mail($$$@) { $message = $stdout; $message_entity = MIME::Entity->build( - Type => 'multipart/encrypted; protocol="application/pgp-encrypted"'); + Type => 'multipart/encrypted; protocol="application/pgp-encrypted"', + Encoding => '7bit'); $message_entity->attach( Type => "application/pgp-encrypted", @@ -705,9 +800,11 @@ sub send_mail($$$@) { $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("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); - $message_entity->send(); + mywarn("You have set arguments to pass to Mail::Mailer. Better fix your MTA. (Also, Mail::Mailer's error reporting is non existant, so it won't tell you when it doesn't work.)") if (scalar @{$CONFIG{'mailer-send'}} > 0); + $message_entity->send(@{$CONFIG{'mailer-send'}}); $message_entity->stringify(); }; @@ -763,8 +860,9 @@ sub delete_signatures($$$$$$) { }; - -my $USER; +################### +# argument handling +################### my @KEYIDS; my $params; @@ -801,17 +899,12 @@ if ($params->{'version'}) { }; usage(\*STDERR, 1) unless scalar @ARGV >= 1; - - -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{'local-user'} = $params->{'local-user'} if defined $params->{'local-user'}; +$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'}; for my $keyid (@ARGV) { $keyid =~ s/^0x//i; @@ -826,13 +919,6 @@ for my $keyid (@ARGV) { 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 ################# @@ -909,6 +995,7 @@ if ($CONFIG{'no-download'}) { # [GNUPG:] NODATA 1 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039 my %local_keyids = map { $_ => 1 } @KEYIDS; + my $had_v3_keys = 0; for my $line (split /\n/, $status) { if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) { my $imported_key = $1; @@ -927,13 +1014,23 @@ if ($CONFIG{'no-download'}) { delete $local_keyids{$speced_key}; unshift @keyids_ok, $imported_key; } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) { + } elsif ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{32})/) { + my $imported_key = $1; + notice ("Imported key $1 is a version 3 key. Version 3 keys are obsolete, should not be used, and are not and will not be properly supported."); + $had_v3_keys = 1; } else { notice ("got unknown reply from gpg: $line"); } }; if (scalar %local_keyids) { - notice ("Import failed for: ". (join ' ', keys %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) { + mywarn("Assuming ". (join ' ', keys %local_keyids)." is a fine keyid."); + } else { + mywarn("Assuming ". (join ' ', keys %local_keyids)." are fine keyids."); + }; + push @keyids_ok, keys %local_keyids; } }; @@ -950,20 +1047,41 @@ if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) { } 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); + }; }; }; @@ -1198,3 +1316,29 @@ for my $keyid (@keyids_ok) { }; }; + +########################### +# 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}