3 # caff -- CA - Fire and Forget
6 # Copyright (c) 2004, 2005 Peter Palfrader <peter@palfrader.org>
7 # Copyright (c) 2005 Christoph Berg <cb@df7cb.de>
11 # Redistribution and use in source and binary forms, with or without
12 # modification, are permitted provided that the following conditions
14 # 1. Redistributions of source code must retain the above copyright
15 # notice, this list of conditions and the following disclaimer.
16 # 2. Redistributions in binary form must reproduce the above copyright
17 # notice, this list of conditions and the following disclaimer in the
18 # documentation and/or other materials provided with the distribution.
19 # 3. The name of the author may not be used to endorse or promote products
20 # derived from this software without specific prior written permission.
22 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
23 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
24 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
25 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
26 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
27 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 caff -- CA - Fire and Forget
43 =item B<caff> [-eEmMRS] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
49 CA Fire and Forget is a script that helps you in keysigning. It takes a list
50 of keyids on the command line, fetches them from a keyserver and calls GnuPG so
51 that you can sign it. It then mails each key to all its email addresses - only
52 including the one UID that we send to in each mail, pruned from all but self
53 sigs and sigs done by you.
59 =item B<-e>, B<--export-old>
61 Export old signatures. Default is to ask the user for each old signature.
63 =item B<-E>, B<--no-export-old>
65 Do not export old signatures. Default is to ask the user for each old
68 =item B<-m>, B<--mail>
70 Send mail after signing. Default is to ask the user for each uid.
72 =item B<-M>, B<--no-mail>
74 Do not send mail after signing. Default is to ask the user for each uid.
76 =item B<-R>, B<--no-download>
78 Do not retrieve the key to be signed from a keyserver.
80 =item B<-S>, B<--no-sign>
84 =item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
86 Select the key that is used for signing, in case you have more than one key.
88 =item B<--key-file> I<file>
90 Import keys from file. Can be supplied more than once.
98 =item $HOME/.caffrc - configuration file
100 =item $HOME/.caff/keys/yyyy-mm-dd/ - processed keys
102 =item $HOME/.caff/gnupghome/ - caff's working dir for gpg
104 =item $HOME/.caff/gnupghome/gpg.conf - gpg configuration
106 useful options include use-agent, default-cert-level, etc.
110 =head1 CONFIGURATION FILE OPTIONS
112 The configuration file is a perl script that sets values in the hash B<%CONFIG>.
113 The file is generated when it does not exist.
117 $CONFIG{owner} = q{Peter Palfrader};
118 $CONFIG{email} = q{peter@palfrader.org};
119 $CONFIG{keyid} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
121 =head2 Required basic settings
125 =item B<owner> [string]
127 Your name. B<REQUIRED>.
129 =item B<email> [string]
131 Your email address, used in From: lines. B<REQUIRED>.
133 =item B<keyid> [list of keyids]
135 A list of your keys. This is used to determine which signatures to keep
136 in the pruning step. If you select a key using B<-u> it has to be in
137 this list. B<REQUIRED>.
139 =head2 General settings
141 =item B<caffhome> [string]
143 Base directory for the files caff stores. Default: B<$HOME/.caff/>.
145 =head2 GnuPG settings
147 =item B<gpg> [string]
149 Path to the GnuPG binary. Default: B<gpg>.
151 =item B<gpg-sign> [string]
153 Path to the GnuPG binary which is used to sign keys. Default: what
156 =item B<gpg-delsig> [string]
158 Path to the GnuPG binary which is used to split off signatures. This was
159 needed while the upstream GnuPG was not fixed. Default: what B<gpg>
162 =item B<secret-keyring> [string]
164 Path to your secret keyring. Default: B<$HOME/.gnupg/secring.gpg>.
166 =item B<also-encrypt-to> [keyid]
168 An additional keyid to encrypt messages to. Default: none.
170 =item B<gpg-sign-args> [string]
172 Additional arguments to pass to gpg. Default: none.
174 =head2 Keyserver settings
176 =item B<keyserver> [string]
178 Keyserver to download keys from. Default: B<subkeys.pgp.net>.
180 =item B<no-download> [boolean]
182 If true, then skip the step of fetching keys from the keyserver.
185 =item B<key-files> [list of files]
187 A list of files containing keys to be imported.
189 =head2 Signing settings
191 =item B<no-sign> [boolean]
193 If true, then skip the signing step. Default: B<0>.
195 =item B<ask-sign> [boolean]
197 If true, then pause before continuing to the signing step.
198 This is useful for offline signing. Default: B<0>.
200 =item B<export-sig-age> [seconds]
202 Don't export UIDs by default, on which your latest signature is older
203 than this age. Default: B<24*60*60> (i.e. one day).
207 =item B<mail> [boolean]
209 Do not prompt for sending mail, just do it. Default: B<0>.
211 =item B<no-mail> [boolean]
213 Do not prompt for sending mail. The messages are still written to
214 $CONFIG{caffhome}/keys/. Default: B<0>.
216 =item B<mail-template> [string]
218 Email template which is used as the body text for the email sent out
219 instead of the default text if specified. The following perl variables
220 can be used in the template:
224 =item B<{owner}> [string]
226 Your name as specified in the L<B<owner>|/item_owner__5bstring_5d> setting.
228 =item B<{key}> [string]
230 The keyid of the key you signed.
232 =item B<{@uids}> [array]
234 The UIDs for which signatures are included in the mail.
238 =item B<bcc> [string]
240 Address to send blind carbon copies to when sending mail.
249 =item Peter Palfrader <peter@palfrader.org>
251 =item Christoph Berg <cb@df7cb.de>
257 http://pgp-tools.alioth.debian.org/
261 gpg(1), pgp-clean(1), /usr/share/doc/signing-party/examples/caffrc.sample.
269 use File
::Temp
qw{tempdir
};
275 use GnuPG
::Interface
;
278 my $REVISION = '$Rev$';
279 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
280 my $VERSION = "0.0.0.$REVISION_NUMER";
282 sub generate_config
() {
283 die "Error: \$LOGNAME is not set.\n" unless $ENV{LOGNAME
};
284 my $gecos = (getpwnam($ENV{LOGNAME
}))[6];
287 my $gpg = GnuPG
::Interface
->new();
289 $gpg->options->hash_init(
290 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
--with
-colons
--fixed
-list
-mode
} ] );
291 $gpg->options->meta_interactive( 0 );
292 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
293 my $pid = $gpg->list_public_keys(handles
=> $handles, command_args
=> [ $gecos ]);
294 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
298 warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
302 unless (@keys = ($stdout =~ /^pub:[^r:]*:(?:[^:]*:){2,2}([^:]+):/mg)) {
303 die "Error: No keys were found using \"gpg --list-public-keys '$gecos'\".\n";
305 unless ($stdout =~ /^uid:.*<(.+@.+)>.*:/m) {
306 die "Error: No email address was found using \"gpg --list-public-keys '$gecos'\".\n";
311 # .caffrc -- vim:syntax=perl:
312 # This file is in perl(1) format - see caff(1) for details.
314 \$CONFIG{'owner'} = '$gecos';
315 \$CONFIG{'email'} = '$email';
317 # you can get your long keyid from
318 # gpg --with-colons --list-key <yourkeyid|name|emailaddress..>
320 # if you have a v4 key, it will simply be the last 16 digits of
323 \$CONFIG{'keyid'} = [ qw{@keys} ];
328 my $config = $ENV{'HOME'} . '/.caffrc';
329 unless (-f
$config) {
330 print "No configfile $config present, I will use this template:\n";
331 my $template = generate_config
();
332 print "$template\nPress enter to continue.";
334 open F
, ">$config" or die "$config: $!";
338 unless (scalar eval `cat $config`) {
339 die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
342 $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'};
343 die ("owner is not defined.\n") unless defined $CONFIG{'owner'};
344 die ("email is not defined.\n") unless defined $CONFIG{'email'};
345 die ("keyid is not defined.\n") unless defined $CONFIG{'keyid'};
346 die ("keyid is not an array ref\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
347 for my $keyid (@
{$CONFIG{'keyid'}}) {
348 $keyid =~ /^[A-F0-9]{16}$/i or die ("key $keyid is not a long (16 digit) keyid.\n");
350 @
{$CONFIG{'keyid'}} = map { uc } @
{$CONFIG{'keyid'}};
351 $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
352 $CONFIG{'keyserver'} = 'subkeys.pgp.net' unless defined $CONFIG{'keyserver'};
353 $CONFIG{'gpg'} = 'gpg' unless defined $CONFIG{'gpg'};
354 $CONFIG{'gpg-sign'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-sign'};
355 $CONFIG{'gpg-delsig'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-delsig'};
356 $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
357 $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
358 $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
359 $CONFIG{'key-files'} = () unless defined $CONFIG{'key-files'};
360 $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
363 please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
364 {foreach $uid (@uids) {
365 $OUT .= "\t".$uid."\n";
366 };} of your key {$key} signed by me.
368 Note that I did not upload your key to any keyservers.
369 If you have multiple user ids, I sent the signature for each user id
370 separately to that user id's associated email address. You can import
371 the signatures by running each through `gpg --import`.
373 If you want this new signature to be available to others, please upload
374 it yourself. With GnuPG this can be done using
375 gpg --keyserver subkeys.pgp.net --send-key {$key}
377 If you have any questions, don't hesitate to ask.
386 print "[NOTICE] $line\n";
390 print "[INFO] $line\n";
394 #print "[DEBUG] $line\n";
398 #print "[trace] $line\n";
402 #print "[trace2] $line\n";
407 stdin
=> IO
::Handle
->new(),
408 stdout
=> IO
::Handle
->new(),
409 stderr
=> IO
::Handle
->new(),
410 status
=> IO
::Handle
->new() );
411 my $handles = GnuPG
::Handles
->new( %fds );
412 return ($fds{'stdin'}, $fds{'stdout'}, $fds{'stderr'}, $fds{'status'}, $handles);
415 sub readwrite_gpg
($$$$$%) {
416 my ($in, $inputfd, $stdoutfd, $stderrfd, $statusfd, %options) = @_;
418 trace
("Entering readwrite_gpg.");
420 my ($first_line, undef) = split /\n/, $in;
421 debug
("readwrite_gpg sends ".(defined $first_line ?
$first_line : "<nothing>"));
423 local $INPUT_RECORD_SEPARATOR = undef;
424 my $sout = IO
::Select
->new();
425 my $sin = IO
::Select
->new();
428 trace
("input is $inputfd; output is $stdoutfd; err is $stderrfd; status is ".(defined $statusfd ?
$statusfd : 'undef').".");
430 $inputfd->blocking(0);
431 $stdoutfd->blocking(0);
432 $statusfd->blocking(0) if defined $statusfd;
433 $stderrfd->blocking(0);
434 $sout->add($stdoutfd);
435 $sout->add($stderrfd);
436 $sout->add($statusfd) if defined $statusfd;
439 my ($stdout, $stderr, $status) = ("", "", "");
440 my $exitwhenstatusmatches = $options{'exitwhenstatusmatches'};
441 trace
("doing stuff until we find $exitwhenstatusmatches") if defined $exitwhenstatusmatches;
443 my $readwrote_stuff_this_time = 0;
444 my $do_not_wait_on_select = 0;
445 my ($readyr, $readyw, $written);
446 while ($sout->count() > 0 || (defined($sin) && ($sin->count() > 0))) {
447 if (defined $exitwhenstatusmatches) {
448 if ($status =~ /$exitwhenstatusmatches/m) {
449 trace
("readwrite_gpg found match on $exitwhenstatusmatches");
450 if ($readwrote_stuff_this_time) {
451 trace
("read/write some more\n");
452 $do_not_wait_on_select = 1;
454 trace
("that's it in our while loop.\n");
460 $readwrote_stuff_this_time = 0;
461 trace
("select waiting for ".($sout->count())." fds.");
462 ($readyr, $readyw, undef) = IO
::Select
::select($sout, $sin, undef, $do_not_wait_on_select ?
0 : 1);
463 trace
("ready: write: ".(defined $readyw ?
scalar @
$readyw : 0 )."; read: ".(defined $readyr ?
scalar @
$readyr : 0));
464 for my $wfd (@
$readyw) {
465 $readwrote_stuff_this_time = 1;
466 if (length($in) != $offset) {
467 trace
("writing to $wfd.");
468 $written = $wfd->syswrite($in, length($in) - $offset, $offset);
471 if ($offset == length($in)) {
472 trace
("writing to $wfd done.");
473 unless ($options{'nocloseinput'}) {
475 trace
("$wfd closed.");
482 next unless (defined(@
$readyr)); # Wait some more.
484 for my $rfd (@
$readyr) {
485 $readwrote_stuff_this_time = 1;
487 trace
("reading from $rfd done.");
492 trace
("reading from $rfd.");
493 if ($rfd == $stdoutfd) {
495 trace2
("stdout is now $stdout\n================");
498 if (defined $statusfd && $rfd == $statusfd) {
500 trace2
("status is now $status\n================");
503 if ($rfd == $stderrfd) {
505 trace2
("stderr is now $stderr\n================");
510 trace
("readwrite_gpg done.");
511 return ($stdout, $stderr, $status);
515 my ($question, $default, $forceyes, $forceno) = @_;
517 my $yn = $default ?
'[Y/n]' : '[y/N]';
519 print $question,' ',$yn, ' ';
520 if ($forceyes && $forceno) {
521 print "$default (from config/command line)\n";
525 print "YES (from config/command line)\n";
529 print "NO (from config/command line)\n";
534 if (!defined $answer) {
535 $OUTPUT_AUTOFLUSH = 1;
537 "End of STDIN reached. Are you using xargs? Caff wants to read from STDIN,\n".
538 "so you can't really use it with xargs. A patch against caff to read from\n".
539 "the terminal would be appreciated.\n".
540 "For now instead of cat keys | xargs caff do caff `cat keys`\n";
543 last if ((length $answer == 0) || ($answer =~ m/^[yYnN]$/) );
544 print "What about $yn is so hard to understand?\nAnswer with either 'n' or 'y' or just press enter for the default.\n";
547 my $result = $default;
548 $result = 1 if $answer =~ /y/i;
549 $result = 0 if $answer =~ /n/i;
557 my $KEYEDIT_PROMPT = '^\[GNUPG:\] GET_LINE keyedit.prompt';
558 my $KEYEDIT_DELUID_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.uid.okay';
559 my $KEYEDIT_DELSIG_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.delsig';
560 my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = '^\[GNUPG:\] (GET_BOOL keyedit.delsig|GET_LINE keyedit.prompt)';
561 my $KEYEDIT_DELSUBKEY_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.subkey';
564 my $USER_AGENT = "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.";
566 my $KEYSBASE = $CONFIG{'caffhome'}.'/keys';
567 my $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
569 -d
$KEYSBASE || mkpath
($KEYSBASE , 0, 0700) or die ("Cannot create $KEYSBASE: $!\n");
570 -d
$GNUPGHOME || mkpath
($GNUPGHOME, 0, 0700) or die ("Cannot create $GNUPGHOME: $!\n");
573 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($NOW);
574 my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday);
579 print $fd "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.\n";
583 my ($fd, $exitcode) = @_;
585 print $fd "Usage: $PROGRAM_NAME [-eEmMRS] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
586 print $fd "Consult the manual page for more information.\n";
591 # export key $keyid from $gnupghome
594 my ($gnupghome, $keyid) = @_;
596 my $gpg = GnuPG
::Interface
->new();
597 $gpg->call( $CONFIG{'gpg'} );
598 if (defined $gnupghome) {
599 $gpg->options->hash_init(
600 'homedir' => $gnupghome,
601 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
} ],
604 $gpg->options->hash_init(
605 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
} ],
608 $gpg->options->meta_interactive( 0 );
609 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
610 my $pid = $gpg->export_keys(handles
=> $handles, command_args
=> [ $keyid ]);
611 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
618 # import a key from the scalar $asciikey into a gpg homedirectory in $tempdir
621 my ($gnupghome, $asciikey) = @_;
623 my $gpg = GnuPG
::Interface
->new();
624 $gpg->call( $CONFIG{'gpg'} );
625 $gpg->options->hash_init(
626 'homedir' => $gnupghome,
627 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
} ] );
628 $gpg->options->meta_interactive( 0 );
629 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
630 my $pid = $gpg->import_keys(handles
=> $handles);
631 my ($stdout, $stderr, $status) = readwrite_gpg
($asciikey, $inputfd, $stdoutfd, $stderrfd, $statusfd);
634 if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
642 # Send an email to $address. If $can_encrypt is true then the mail
643 # will be PGP/MIME encrypted to $longkeyid.
645 # $longkeyid, $uid, and @attached will be used in the email and the template.
647 #send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
648 sub send_mail
($$$@
) {
649 my ($address, $can_encrypt, $key_id, @keys) = @_;
651 my $template = Text
::Template
->new(TYPE
=> 'STRING', SOURCE
=> $CONFIG{'mail-template'})
652 or die "Error creating template: $Text::Template::ERROR";
655 for my $key (@keys) {
656 push @uids, $key->{'text'};
658 my $message = $template->fill_in(HASH
=> { key
=> $key_id,
660 owner
=> $CONFIG{'owner'}})
661 or die "Error filling template in: $Text::Template::ERROR";
663 my $message_entity = MIME
::Entity
->build(
664 Type
=> "text/plain",
666 Disposition
=> 'inline',
670 for my $key (@keys) {
671 $message_entity->attach(
672 Type
=> "application/pgp-keys",
673 Disposition
=> 'attachment',
675 Description
=> "PGP Key 0x$key_id, uid ".($key->{'text'}).' ('.($key->{'serial'}).'), signed by 0x'.$CONFIG{'keyid'}[0],
676 Data
=> $key->{'key'},
677 Filename
=> "0x$key_id.".$key->{'serial'}.".signed-by-0x".$CONFIG{'keyid'}[0].".asc");
681 my $message = $message_entity->stringify();
683 my $gpg = GnuPG
::Interface
->new();
684 $gpg->call( $CONFIG{'gpg'} );
685 $gpg->options->hash_init( 'homedir' => $GNUPGHOME,
686 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
} ],
688 $gpg->options->meta_interactive( 0 );
689 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
690 $gpg->options->push_recipients( $key_id );
691 $gpg->options->push_recipients( $CONFIG{'also-encrypt-to'} ) if defined $CONFIG{'also-encrypt-to'};
692 my $pid = $gpg->encrypt(handles
=> $handles);
693 my ($stdout, $stderr, $status) = readwrite_gpg
($message, $inputfd, $stdoutfd, $stderrfd, $statusfd);
696 warn ("No data from gpg for list-key $key_id\n");
701 $message_entity = MIME
::Entity
->build(
702 Type
=> 'multipart/encrypted; protocol="application/pgp-encrypted"');
704 $message_entity->attach(
705 Type
=> "application/pgp-encrypted",
706 Disposition
=> 'attachment',
708 Data
=> "Version: 1\n");
710 $message_entity->attach(
711 Type
=> "application/octet-stream",
712 Filename
=> 'msg.asc',
713 Disposition
=> 'inline',
718 $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
719 $message_entity->head->add("To", $address);
720 $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
721 $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
722 $message_entity->head->add("User-Agent", $USER_AGENT);
723 $message_entity->send();
724 $message_entity->stringify();
728 # clean up a UID so that it can be used on the FS.
730 sub sanitize_uid
($) {
734 $good_uid =~ tr
#/:\\#_#;
735 trace2
("[sanitize_uid] changed UID from $uid to $good_uid.\n") if $good_uid ne $uid;
739 sub delete_signatures
($$$$$$) {
740 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $keyids) =@_;
742 my $signed_by_me = 0;
744 my ($stdout, $stderr, $status) =
745 readwrite_gpg
("delsig\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_DELSIG_PROMPT, nocloseinput
=> 1);
747 while($status =~ /$KEYEDIT_DELSIG_PROMPT/m) {
748 # sig:?::17:EA2199412477CAF8:1058095214:::::13x:
749 my @sigline = grep { /^sig/ } (split /\n/, $stdout);
750 $stdout =~ s/\n/\\n/g;
751 notice
("[sigremoval] why are there ".(scalar @sigline)." siglines in that part of the dialog!? got: $stdout") if scalar @sigline >= 2; # XXX
752 my $line = pop @sigline;
754 if (defined $line) { # only if we found a sig here - we never remove revocation packets for instance
755 debug
("[sigremoval] doing line $line.");
756 my (undef, undef, undef, undef, $signer, $created, undef, undef, undef) = split /:/, $line;
757 if ($signer eq $longkeyid) {
758 debug
("[sigremoval] selfsig ($signer).");
760 } elsif (grep { $signer eq $_ } @
{$keyids}) {
761 debug
("[sigremoval] signed by us ($signer).");
763 $signed_by_me = $signed_by_me > $created ?
$signed_by_me : $created;
765 debug
("[sigremoval] not interested in that sig ($signer).");
769 debug
("[sigremoval] no sig line here, only got: ".$stdout);
771 ($stdout, $stderr, $status) =
772 readwrite_gpg
($answer."\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT, nocloseinput
=> 1);
775 return $signed_by_me;
784 Getopt
::Long
::config
('bundling');
786 '-h' => \
$params->{'help'},
787 '--help' => \
$params->{'help'},
788 '--version' => \
$params->{'version'},
789 '-V' => \
$params->{'version'},
790 '-u=s' => \
$params->{'local-user'},
791 '--local-user=s' => \
$params->{'local-user'},
792 '-e' => \
$params->{'export-old'},
793 '--export-old' => \
$params->{'export-old'},
794 '-E' => \
$params->{'no-export-old'},
795 '--no-export-old' => \
$params->{'no-export-old'},
796 '-m' => \
$params->{'mail'},
797 '--mail' => \
$params->{'mail'},
798 '-M' => \
$params->{'no-mail'},
799 '--no-mail' => \
$params->{'no-mail'},
800 '-R' => \
$params->{'no-download'},
801 '--no-download' => \
$params->{'no-download'},
802 '-S' => \
$params->{'no-sign'},
803 '--no-sign' => \
$params->{'no-sign'},
804 '--key-file=s@' => \
$params->{'key-files'},
808 if ($params->{'help'}) {
811 if ($params->{'version'}) {
815 usage
(\
*STDERR
, 1) unless scalar @ARGV >= 1;
819 if ($params->{'local-user'}) {
820 $USER = $params->{'local-user'};
822 unless ($USER =~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
823 print STDERR
"-u $USER is not a keyid.\n";
829 for my $keyid (@ARGV) {
831 unless ($keyid =~ /^([A-F0-9]{8}|[A-F0-9]{16}||[A-F0-9]{40})$/i) {
832 if ($keyid =~ /^[A-F0-9]{32}$/) {
833 info
("Ignoring v3 fingerprint $keyid. v3 keys are obsolete.");
836 print STDERR
"$keyid is not a keyid.\n";
839 push @KEYIDS, uc($keyid);
842 $CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
843 $CONFIG{'no-mail'} = $params->{'no-mail'} if defined $params->{'no-mail'};
844 $CONFIG{'mail'} = $params->{'mail'} if defined $params->{'mail'};
845 $CONFIG{'no-sign'} = $params->{'no-sign'} if defined $params->{'no-sign'};
846 push @
{$CONFIG{'key-files'}}, @
{$params->{'key-files'}} if defined $params->{'key-files'};
852 for my $keyid (@
{$CONFIG{'keyid'}}) {
853 my $gpg = GnuPG
::Interface
->new();
854 $gpg->call( $CONFIG{'gpg'} );
855 $gpg->options->hash_init(
856 'homedir' => $GNUPGHOME,
857 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
--with
-colons
--fixed
-list
-mode
--fast
-list
-mode
} ] );
858 $gpg->options->meta_interactive( 0 );
859 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
860 my $pid = $gpg->list_public_keys(handles
=> $handles, command_args
=> $keyid);
861 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
865 warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
867 unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
868 info
("Key $keyid not found in caff's home. Getting it from your normal GnuPGHome.");
869 my $key = export_key
(undef, $keyid);
870 if (!defined $key || $key eq '') {
871 warn ("Did not get key $keyid from your normal GnuPGHome\n");
874 my $result = import_key
($GNUPGHOME, $key);
876 warn ("Could not import $keyid into caff's gnupghome.\n");
882 ########################
883 # import keys from files
884 ########################
885 foreach my $keyfile (@
{$CONFIG{'key-files'}}) {
886 my $gpg = GnuPG
::Interface
->new();
887 $gpg->call( $CONFIG{'gpg'} );
888 $gpg->options->hash_init('homedir' => $GNUPGHOME);
889 $gpg->options->meta_interactive( 0 );
890 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
891 my $pid = $gpg->import_keys(handles
=> $handles, command_args
=> $keyfile);
892 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
893 info
("Importing keys from $keyfile");
895 if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
900 #############################
901 # receive keys from keyserver
902 #############################
904 if ($CONFIG{'no-download'}) {
905 @keyids_ok = @KEYIDS;
907 info
("fetching keys, this will take a while...");
909 my $gpg = GnuPG
::Interface
->new();
910 $gpg->call( $CONFIG{'gpg'} );
911 $gpg->options->hash_init(
912 'homedir' => $GNUPGHOME,
913 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
}, '--keyserver='.$CONFIG{'keyserver'} ] );
914 $gpg->options->meta_interactive( 0 );
915 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
916 my $pid = $gpg->recv_keys(handles
=> $handles, command_args
=> [ @KEYIDS ]);
917 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
920 # [GNUPG:] IMPORT_OK 0 5B00C96D5D54AEE1206BAF84DE7AAF6E94C09C7F
923 # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
924 my %local_keyids = map { $_ => 1 } @KEYIDS;
925 for my $line (split /\n/, $status) {
926 if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
927 my $imported_key = $1;
928 my $whole_fpr = $imported_key;
929 my $long_keyid = substr($imported_key, -16);
930 my $short_keyid = substr($imported_key, -8);
932 for my $spec (($whole_fpr, $long_keyid, $short_keyid)) {
933 $speced_key = $spec if $local_keyids{$spec};
935 unless ($speced_key) {
936 notice
("Imported unexpected key; got: $imported_key\n");
939 debug
("Imported $imported_key for $speced_key");
940 delete $local_keyids{$speced_key};
941 unshift @keyids_ok, $imported_key;
942 } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) {
944 notice
("got unknown reply from gpg: $line");
947 if (scalar %local_keyids) {
948 notice
("Import failed for: ". (join ' ', keys %local_keyids).".");
949 exit 1 unless ask
("Some keys could not be imported - continue anyway?", 0);
953 unless (@keyids_ok) {
954 notice
("No keys to sign found");
961 if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) {
962 $CONFIG{'no-sign'} = ! ask
("Continue with signing?", 1);
965 unless ($CONFIG{'no-sign'}) {
966 info
("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
967 for my $keyid (@keyids_ok) {
969 push @command, $CONFIG{'gpg-sign'};
970 push @command, '--local-user', $USER if (defined $USER);
971 push @command, "--homedir=$GNUPGHOME";
972 push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
973 push @command, '--no-auto-check-trustdb';
974 push @command, '--trust-model=always';
975 push @command, '--edit', $keyid;
976 push @command, 'sign';
977 push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
978 print join(' ', @command),"\n";
987 for my $keyid (@keyids_ok) {
990 my $gpg = GnuPG
::Interface
->new();
991 $gpg->call( $CONFIG{'gpg'} );
992 $gpg->options->hash_init(
993 'homedir' => $GNUPGHOME,
994 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
--with
-colons
--fixed
-list
-mode
} ] );
995 $gpg->options->meta_interactive( 0 );
996 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
997 my $pid = $gpg->list_public_keys(handles
=> $handles, command_args
=> [ $keyid ]);
998 my ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
1000 if ($stdout eq '') {
1001 warn ("No data from gpg for list-key $keyid\n");
1004 my @publine = grep { /^pub/ } (split /\n/, $stdout);
1005 if (scalar @publine == 0) {
1006 warn ("No public keys found with list-key $keyid (note that caff uses its own keyring in $GNUPGHOME).\n");
1009 my (undef, undef, undef, undef, $longkeyid, undef, undef, undef, undef, undef, undef, $flags) = split /:/, pop @publine;
1010 if (scalar @publine > 0) {
1011 warn ("More than one key matched $keyid. Try to specify the long keyid or fingerprint\n");
1014 unless (defined $longkeyid) {
1015 warn ("Didn't find public keyid in --list-key of key $keyid.\n");
1018 unless (defined $flags) {
1019 warn ("Didn't find flags in --list-key of key $keyid.\n");
1022 my $can_encrypt = $flags =~ /E/;
1026 my $asciikey = export_key
($GNUPGHOME, $keyid);
1027 if ($asciikey eq '') {
1028 warn ("No data from gpg for export $keyid\n");
1035 my $this_uid_text = '';
1037 debug
("Doing key $keyid, uid $uid_number");
1038 my $tempdir = tempdir
( "caff-$keyid-XXXXX", DIR
=> '/tmp/', CLEANUP
=> 1);
1040 # import into temporary gpghome
1041 ###############################
1042 my $result = import_key
($tempdir, $asciikey);
1044 warn ("Could not import $keyid into temporary gnupg.\n");
1050 $gpg = GnuPG
::Interface
->new();
1051 $gpg->call( $CONFIG{'gpg-delsig'} );
1052 $gpg->options->hash_init(
1053 'homedir' => $tempdir,
1054 'extra_args' => [ qw{ --no-auto
-check
-trustdb
--trust
-model
=always
--with
-colons
--fixed
-list
-mode
--command
-fd
=0 --no-tty
} ] );
1055 ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds
();
1056 $pid = $gpg->wrap_call(
1057 commands
=> [ '--edit' ],
1058 command_args
=> [ $keyid ],
1059 handles
=> $handles );
1061 debug
("Starting edit session");
1062 ($stdout, $stderr, $status) = readwrite_gpg
('', $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1066 my $number_of_subkeys = 0;
1070 my $delete_some = 0;
1071 debug
("Parsing stdout output.");
1072 for my $line (split /\n/, $stdout) {
1073 debug
("Checking line $line");
1074 my ($type, undef, undef, undef, undef, undef, undef, undef, undef, $uidtext) = split /:/, $line;
1075 if ($type eq 'sub') {
1076 $number_of_subkeys++;
1078 next unless ($type eq 'uid' || $type eq 'uat');
1079 debug
("line is interesting.");
1080 if ($uid_number != $i) {
1081 debug
("mark for deletion.");
1082 readwrite_gpg
("$i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1087 $this_uid_text = ($type eq 'uid') ?
$uidtext : '[attribute]';
1088 $is_uat = $type eq 'uat';
1092 debug
("Parsing stdout output done.");
1093 unless ($have_one) {
1094 debug
("Uid ".($uid_number-1)." was the last, there is no $uid_number.");
1095 info
("key $keyid done.");
1099 my $prune_some_sigs_on_uid;
1100 my $prune_all_sigs_on_uid;
1102 debug
("handling attribute userid of key $keyid.");
1103 if ($uid_number == 1) {
1104 debug
(" attribute userid is #1, unmarking #2 for deletion.");
1105 readwrite_gpg
("2\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1107 $prune_some_sigs_on_uid = 1;
1108 $prune_all_sigs_on_uid = 2;
1110 debug
("attribute userid is not #1, unmarking #1 for deletion.");
1111 readwrite_gpg
("1\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1113 $prune_some_sigs_on_uid = 2;
1114 $prune_all_sigs_on_uid = 1;
1117 $prune_some_sigs_on_uid = 1;
1121 debug
("need to delete $delete_some uids.");
1122 readwrite_gpg
("deluid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_DELUID_PROMPT, nocloseinput
=> 1);
1123 readwrite_gpg
("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1128 if ($number_of_subkeys > 0) {
1129 for (my $i=1; $i<=$number_of_subkeys; $i++) {
1130 readwrite_gpg
("key $i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1132 readwrite_gpg
("delkey\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_DELSUBKEY_PROMPT, nocloseinput
=> 1);
1133 readwrite_gpg
("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1);
1138 readwrite_gpg
("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # mark uid for delsig
1139 my $signed_by_me = delete_signatures
($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $CONFIG{'keyid'});
1140 readwrite_gpg
("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # unmark uid from delsig
1141 if (defined $prune_all_sigs_on_uid) {
1142 readwrite_gpg
("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # mark uid for delsig
1143 delete_signatures
($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, []);
1144 readwrite_gpg
("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches
=> $KEYEDIT_PROMPT, nocloseinput
=> 1); # unmark uid from delsig
1148 readwrite_gpg
("save\n", $inputfd, $stdoutfd, $stderrfd, $statusfd);
1151 my $asciikey = export_key
($tempdir, $keyid);
1152 if ($asciikey eq '') {
1153 warn ("No data from gpg for export $keyid\n");
1157 if ($signed_by_me) {
1158 if ($NOW - $signed_by_me > $CONFIG{'export-sig-age'} ) {
1159 my $write = ask
("Signature on $this_uid_text is old. Export?", 0, $params->{'export-old'}, $params->{'no-export-old'});
1162 my $keydir = "$KEYSBASE/$DATE_STRING";
1163 -d
$keydir || mkpath
($keydir , 0, 0700) or die ("Cannot create $keydir $!\n");
1165 my $keyfile = "$keydir/$longkeyid.key.$uid_number.".sanitize_uid
($this_uid_text).".asc";
1166 open (KEY
, ">$keyfile") or die ("Cannot open $keyfile: $!\n");
1167 print KEY
$asciikey;
1170 push @UIDS, { text
=> $this_uid_text, key
=> $asciikey, serial
=> $uid_number, "is_uat" => $is_uat };
1172 info
("$longkeyid $uid_number $this_uid_text done.");
1174 info
("$longkeyid $uid_number $this_uid_text is not signed by me, not writing.");
1178 if (scalar @UIDS == 0) {
1179 info
("found no signed uids for $keyid");
1181 next if $CONFIG{'no-mail'}; # do not send mail
1184 for my $uid (@UIDS) {
1185 trace
("UID: $uid->{'text'}\n");
1186 if ($uid->{'is_uat'}) {
1187 my $attach = ask
("UID $uid->{'text'} is an attribute UID, attach it to every email sent?", 1);
1188 push @attached, $uid if $attach;
1189 } elsif ($uid->{'text'} !~ /@/) {
1190 my $attach = ask
("UID $uid->{'text'} is no email address, attach it to every email sent?", 1);
1191 push @attached, $uid if $attach;
1195 notice
("Key has no encryption capabilities, mail will be sent unencrypted") unless $can_encrypt;
1196 for my $uid (@UIDS) {
1197 if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
1198 my $address = $uid->{'text'};
1199 $address =~ s/.*<(.*)>.*/$1/;
1200 if (ask
("Mail signature for $uid->{'text'} to '$address'?", 1, $CONFIG{'mail'})) {
1201 my $mail = send_mail
($address, $can_encrypt, $longkeyid, $uid, @attached);
1203 my $keydir = "$KEYSBASE/$DATE_STRING";
1204 my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid
($uid->{'text'});
1205 open (KEY
, ">$mailfile") or die ("Cannot open $mailfile: $!\n");