Bump revision number to match CVS revision number. Replaced all signature

verification code with code that uses detached signatures.  Signatures
generated by GnuPG couldn't be verified using attached signatures without
adding a Hash header, and this was the path of least resistance plus
avoids munging problems in the future.  Code taken from PGP::Sign.
This commit is contained in:
Russ Allbery 2003-07-07 02:35:00 +00:00
parent 46278eb3e3
commit dea04e918e

386
pgpverify
View file

@ -1,16 +1,28 @@
#! /usr/bin/perl -ws
do '@LIBDIR@/innshellvars.pl';
# Remove the above line if not running as part of INN.
#! /usr/bin/perl -w
# do '@LIBDIR@/innshellvars.pl';
# If running inside INN, uncomment the above and point to innshellvars.pl.
#
# written April 1996, tale@isc.org (David C Lawrence)
# Currently maintained by Russ Allbery <rra@stanford.edu>
# Version 1.15, 25 Nov 2002
# Version 1.22, 2003-07-06
#
# NOTICE TO INN MAINTAINERS: The version that is shipped with INN
# is the same as the version that I make available to the rest of the
# world (including non-INN sites), so please make all changes through me.
# NOTICE TO INN MAINTAINERS: The version that is shipped with INN is the
# same as the version that I make available to the rest of the world
# (including non-INN sites), so please make all changes through me.
#
# This program is intended to be compatible with Perl 4 and Perl 5.
# This program requires Perl 5, probably at least about Perl 5.003 since
# that's when FileHandle was introduced. If you want to use this program
# and your Perl is too old, please contact me (rra@stanford.edu) and tell
# me about it; I want to know what old versions of Perl are still used in
# practice.
#
# Changes from 1.15 -> 1.22
# -- Bump version number to match CVS revision number.
# -- Replaced all signature verification code with code that uses detached
# signatures. Signatures generated by GnuPG couldn't be verified using
# attached signatures without adding a Hash header, and this was the
# path of least resistance plus avoids munging problems in the future.
# Code taken from PGP::Sign.
#
# Changes from 1.14 -> 1.15
# -- Added POD documentation.
@ -53,8 +65,8 @@ do '@LIBDIR@/innshellvars.pl';
# Changes from 1.9 -> 1.10
# -- minor diddling for INN 2.0: use $inn'pathtmp if it exists, and
# work with the new subst method to find innshellvars.pl
# -- do not truncate the tmp file when opening, in case it is really linked
# to another file
# -- do not truncate the tmp file when opening, in case it is really
# linked to another file
#
# Changes from 1.8 -> 1.9
# -- match 'Bad signature' pgp output to return exit status 3 by removing
@ -67,14 +79,17 @@ do '@LIBDIR@/innshellvars.pl';
# -- parse PGP 5.0 'good signature' lines.
# -- allow -test swtich; prints pgp input and output
# -- look for pgp in INN's innshellvars.pl
# -- changed regexp delimiters for stripping $0 to be compatible with old perl
# -- changed regexp delimiters for stripping $0 to be compatible with old
# perl
#
# Changes from 1.5 -> 1.6
# -- handle articles encoded in NNTP format ('.' starting line is doubled,
# \r\n at line end) by stripping NNTP encoding.
# -- exit 255 with pointer to $HOME or $PGPPATH if pgp can't find key ring.
# (probably doesn't match the necessary error message with ViaCrypt PGP)
# -- failures also report message-id so the article can be looked up to retry.
# -- exit 255 with pointer to $HOME or $PGPPATH if pgp can't find key
# ring. (probably doesn't match the necessary error message with
# ViaCrypt PGP)
# -- failures also report message-id so the article can be looked up to
# retry.
#
# Changes from 1.4 -> 1.5
# -- force English lanugage for 'Good signature from user' by passing
@ -94,8 +109,8 @@ do '@LIBDIR@/innshellvars.pl';
# file, the value of $inn::gpgv will override this.
# $gpgv = '/usr/local/bin/gpgv';
# Path to pgp binary; for PGP 5.0, set the path to the pgpv binary.
# If you have INN and the script is able to successfully include your
# Path to pgp binary; for PGP 5.0, set the path to the pgpv binary. If
# you have INN and the script is able to successfully include your
# innshellvars.pl file, the value of $inn::pgp will override this.
$pgp = '/usr/local/bin/pgp';
@ -129,9 +144,9 @@ $lockdir = $tmpdir;
# the script will search some known directories for that program. If it
# can't be found & used, everything falls back on stderr logging.
#
# You can test the script's syslogging by running "pgpverify < /some/text/file"
# on a file that is not a valid news article. The "non-header at line #"
# error should be syslogged.
# You can test the script's syslogging by running "pgpverify <
# /some/text/file" on a file that is not a valid news article. The
# "non-header at line #" error should be syslogged.
#
# $syslog_method = 'unix'; # Unix doman socket, perl5.004_03 or higher
# $syslog_method = 'inet'; # UDP to port 514 of localhost
@ -148,8 +163,8 @@ $syslog_method = 'logger'; # search for the logger program
$syslog_facility = 'news';
$syslog_level = 'err';
# Prepend the error message with a timestamp?
# This is only relevant if not syslogging, when errors go to stderr.
# Prepend the error message with a timestamp? This is only relevant if
# not syslogging, when errors go to stderr.
#
# $log_date = 0; # zero means don't do it.
# $log_date = 1; # non-zero means do it.
@ -164,6 +179,10 @@ use strict;
use vars qw($gpgv $pgp $keyring $tmp $tmpdir $lockdir $syslog_method
$syslog_facility $syslog_level $log_date $test $messageid);
use Fcntl qw(O_WRONLY O_CREAT O_EXCL);
use FileHandle;
use IPC::Open3 qw(open3);
# Turn on test mode if the first argument is '-test'.
if ($1 && $1 eq '-test') {
shift @ARGV;
@ -200,17 +219,30 @@ if ($gpgv) {
# Parse the article headers and generate the PGP message.
my ($nntp_format, $header, $dup) = &parse_header();
exit 1 unless $$header{'X-PGP-Sig'};
my $message = &generate_message($nntp_format, $header, $dup);
&write_message($message);
my ($message, $signature, $version)
= &generate_message($nntp_format, $header, $dup);
# The call to pgp needs to be locked because it tries to both read and
# write a file named randseed.bin but doesn't do its own locking as it
# should, and the consequences of a multiprocess conflict is failure to
# verify.
my $lock;
unless ($gpgv) {
$lock = "$lockdir/LOCK.$0";
until (&shlock($lock) > 0) {
sleep(2);
}
}
# Verify the message.
my ($ok, $signer);
if ($gpgv) {
($ok, $signer) = &gpg_check($tmp, $keyring);
} else {
($ok, $signer) = &pgp_check($tmp, $keyring);
my ($ok, $signer) = pgp_verify($signature, $version, $message);
unless ($gpgv) {
unlink ($lock) or &errmsg("$0: unlink $lock: $!\n");
}
print "$signer\n" if $signer;
unless ($ok == 0) {
&fail("$0: verification failed\n");
}
exit $ok;
@ -245,15 +277,16 @@ sub parse_header {
return ($nntp_format, \%header, \%dup);
}
# Generate the PGP message to verify, undoing the same transformation as
# is applied by signcontrol (along with other changes required to deal
# with NNTP wire format and to quote the message properly for PGP). Takes
# the hash of headers and header duplicates returned by parse_header.
# Generate the PGP message to verify. Takes a flag indicating wire
# format, the hash of headers and header duplicates returned by
# parse_header and returns a list of three elements. The first is the
# message to verify, the second is the signature, and the third is the
# version number.
sub generate_message {
my ($nntp_format, $header, $dup) = @_;
# The regexp below might be too strict about the structure of pgp sig
# lines.
# The regexp below might be too strict about the structure of PGP
# signature lines.
# The $sep value means the separator between the radix64 signature lines
# can have any amount of spaces or tabs, but must have at least one
@ -271,9 +304,9 @@ sub generate_message {
my ($version, $signed_headers, $signature) = ($1, $3, $4);
$signature =~ s/$sep/\n/g;
$signature =~ s/^\s+//;
my $message = "-----BEGIN PGP SIGNED MESSAGE-----\n\n";
$message .= "X-Signed-Headers: $signed_headers\n";
my $message = "X-Signed-Headers: $signed_headers\n";
my $label;
foreach $label (split(",", $signed_headers)) {
&fail("$0: duplicate signed $label header, can't verify\n")
@ -295,146 +328,165 @@ sub generate_message {
s/^\.\./\./;
s/\r\n$/\n/;
}
s/^-/- -/; # pgp quote ("ASCII armor") dashes
$message .= $_; # append to output string
$message .= $_;
}
$message .= "\n-----BEGIN PGP SIGNATURE-----\n";
$message .= "Version: $version\n";
$message .= $signature;
$message .= "\n-----END PGP SIGNATURE-----\n";
return $message;
# Strip off all trailing whitespace for compatibility with the way that
# pgpverify used to work, using attached signatures.
$message =~ s/[ \t]+\n/\n/g;
return ($message, $signature, $version);
}
# Write a PGP message to a file. Attempt to do so safely.
sub write_message {
my ($message) = @_;
# Check a detatched signature for given data. Takes a signature block (in
# the form of an ASCII-armored string with embedded newlines), a version
# number (which may be undef), and the message. We return an exit status
# and the key id if the signature verified. 0 means good signature, 1
# means bad data, 2 means an unknown signer, and 3 means a bad signature.
# In the event of an error, we report with errmsg.
#
# This code is taken almost verbatim from PGP::Sign except for the code to
# figure out the PGP style.
sub pgp_verify {
my ($signature, $version, $message) = @_;
chomp $signature;
open(TMP,">> $tmp") || &fail("$0: open > $tmp: $!\n");
# Ignore SIGPIPE, since we're going to be talking to PGP.
local $SIG{PIPE} = 'IGNORE';
-f TMP ||
&fail("$0: $tmp not a plain file, possible security violation attempt\n");
(stat(_))[3] == 1 ||
&fail("$0: $tmp has hard links, possible security violation attempt\n");
# Set the PGP style based on whether $gpgv is set.
my $pgpstyle = ($gpgv ? 'GPG' : 'PGP2');
seek(TMP, 0, 0); # make sure pointer is at beginning of file
truncate(TMP, 0); # make sure file is zero length
print TMP $message;
close(TMP) || &errmsg("$0: close > $tmp: $!\n");
&fail("$0: write error for message to check\n")
if -s $tmp != length($message);
print $message if $test;
}
# Check the signature using PGP (including 2.6.2, 5.0, and the pgpgpg
# wrapper for GnuPG).
sub pgp_check {
my ($file, $ring) = @_;
$ENV{'PGPPATH'} = $ring if $ring;
# The call to pgp needs to be locked because it tries to both read and
# write a file named randseed.bin but doesn't do its own locking as it
# should, and the consequences of a multiprocess conflict is failure to
# verify.
my $lock = "$lockdir/LOCK.$0";
until (&shlock($lock) > 0) {
sleep(2);
# Because this is a detached signature, we actually need to save both
# the signature and the data to files and then run PGP on the signature
# file to make it verify the signature. Because this is a detached
# signature, though, we don't have to do any data mangling, which makes
# our lives much easier. It would be nice to do this without having to
# use temporary files, but I don't see any way to do so without running
# into mangling problems.
#
# PGP v5 *requires* there be some subheader or another. *sigh*. So we
# supply one if Version isn't given. :)
my $umask = umask 077;
my $filename = $tmpdir . '/pgp' . time . '.' . $$;
my $sigfile = new FileHandle "$filename.asc", O_WRONLY|O_EXCL|O_CREAT;
unless ($sigfile) {
&errmsg ("Unable to open temp file $filename.asc: $!\n");
return (255, undef);
}
open(PGP,"$pgp -f +language=en < $file 2>&1 >/dev/null |") ||
&fail("$0: failed to execute pgp: $!\n");
undef $/;
$_ = <PGP>;
unlink($lock) || &errmsg("$0: unlink $lock: $!\n");
unlink($file) || &errmsg("$0: unlink $file: $!\n");
unless (close(PGP)) {
if ($? >> 8) {
&errmsg("$0: pgp exited status " . ($? >> 8) . "\n");
} else {
&errmsg("$0: pgp died on signal " . ($? & 255) . "\n");
}
}
print if $test;
# MIT PGP 2.6.2:
# Good signature from user "Robert Braver <rbraver@ohww.norman.ok.us>".
# ViaCrypt PGP 4.0:
# Good signature from user: Robert Braver <rbraver@ohww.norman.ok.us>
# GnuPG (via pgpgpg)
# Good signature from "news.announce.newgroups"
# PGP 5.0i:
# Good signature made 1997-07-09 21:57 GMT by key:
# 1024 bits, Key ID B88DA9C1, Created 1996-04-10
# "news.announce.newgroups"
my $ok = 2; # unknown signature result is default
my $signer;
if (/B[Aa][Dd] signature /) {
$ok = 3;
} elsif (/Good signature from user(: (.*)| "(.*)"\.)/ ||
/Good signature from "(.*)"/ ||
/Good signature made .* by key:\n.+\n +"(.*)"/) {
$ok = 0;
$signer = $+;
} elsif (/Keyring file '(.*)' does not exist/) {
&fail("$0: couldn't access $1. Bad \$HOME or \$PGPPATH?\n");
}
return ($ok, $signer);
}
# Check the signature using GnuPG.
sub gpg_check {
my ($file, $ring) = @_;
my $opts = '--quiet --status-fd=1 --logger-fd=1';
if ($ring) {
$opts .= " --keyring=$ring/pubring.gpg";
if ($pgpstyle eq 'PGP2') {
print $sigfile "-----BEGIN PGP MESSAGE-----\n";
} else {
$opts .= ' --keyring=pubring.gpg';
print $sigfile "-----BEGIN PGP SIGNATURE-----\n";
}
if (defined $version) {
print $sigfile "Version: $version\n";
} elsif ($pgpstyle ne 'GPG') {
print $sigfile "Comment: Use GnuPG; it's better :)\n";
}
print $sigfile "\n", $signature;
if ($pgpstyle eq 'PGP2') {
print $sigfile "\n-----END PGP MESSAGE-----\n";
} else {
print $sigfile "\n-----END PGP SIGNATURE-----\n";
}
close $sigfile;
# Signature saved. Now save the actual message.
my $datafile = new FileHandle "$filename", O_WRONLY|O_EXCL|O_CREAT;
unless ($datafile) {
&errmsg ("Unable to open temp file $filename: $!\n");
unlink "$filename.asc";
return (255, undef);
}
print $datafile $message;
close $datafile;
# Figure out what command line we'll be using.
my @command;
if ($pgpstyle eq 'GPG') {
@command = ($gpgv, qw/--quiet --status-fd=1 --logger-fd=1/);
} else {
@command = ($pgp, '+batchmode');
}
open(PGP, "$gpgv $opts $file 2> /dev/null |") ||
&fail("$0: failed to execute $gpgv: $!\n");
# Now, call PGP to check the signature. Because we've written
# everything out to a file, this is actually fairly simple; all we need
# to do is grab stdout. PGP prints its banner information to stderr, so
# just ignore stderr. Set PGPPATH if desired.
local $ENV{PGPPATH} = $keyring if ($keyring && $pgpstyle ne 'GPG');
if ($keyring && $pgpstyle eq 'GPG') {
push (@command, "--keyring=$keyring/pubring.gpg");
}
push (@command, "$filename.asc");
push (@command, $filename);
my $input = new FileHandle;
my $output = new FileHandle;
my $pid = eval { open3 ($input, $output, $output, @command) };
if ($@) {
&errmsg ($@);
&errmsg ("Execution of $command[0] failed.\n");
unlink ($filename, "$filename.asc");
return (255, undef);
}
close $input;
undef $/;
$_ = <PGP>;
unlink($file) || &errmsg("$0: unlink $file: $!\n");
unless (close(PGP)) {
if ($? >> 8) {
&errmsg("$0: gpgv exited status " . ($? >> 8) . "\n");
# Check for the message that gives us the key status and return the
# appropriate thing to our caller. This part is a zoo due to all of the
# different formats used. GPG has finally done the right thing and
# implemented a separate status stream with parseable data.
#
# MIT PGP 2.6.2 and PGP 6.5.2:
# Good signature from user "Russ Allbery <rra@stanford.edu>".
# ViaCrypt PGP 4.0:
# Good signature from user: Russ Allbery <rra@stanford.edu>
# PGP 5.0:
# Good signature made 1999-02-10 03:29 GMT by key:
# 1024 bits, Key ID 0AFC7476, Created 1999-02-10
# "Russ Allbery <rra@stanford.edu>"
#
# Also, PGP v2 prints out "Bad signature" while PGP v5 uses "BAD
# signature", and PGP v6 reverts back to "Bad signature".
local $_;
local $/ = '';
my $signer;
my $ok = 255;
while (<$output>) {
if ($pgpstyle eq 'GPG') {
if (/\[GNUPG:\]\s+GOODSIG\s+\S+\s+(\S+)/) {
$ok = 0;
$signer = $1;
} elsif (/\[GNUPG:\]\s+NODATA/ || /\[GNUPG:\]\s+UNEXPECTED/) {
$ok = 1;
} elsif (/\[GNUPG:\]\s+NO_PUBKEY/) {
$ok = 2;
} elsif (/\[GNUPG:\]\s+BADSIG\s+/) {
$ok = 3;
}
} else {
&errmsg("$0: gpgv died on signal " . ($? & 255) . "\n");
if (/^Good signature from user(?::\s+(.*)|\s+\"(.*)\"\.)$/m) {
$signer = $+;
$ok = 0;
last;
} elsif (/^Good signature made .* by key:\n.+\n\s+\"(.*)\"/m) {
$signer = $1;
$ok = 0;
last;
} elsif (/^\S+: Good signature from \"(.*)\"/m) {
$signer = $1;
$ok = 0;
last;
} elsif (/^(?:\S+: )?Bad signature /im) {
$ok = 3;
last;
}
}
}
print if $test;
my $ok = 255; # default exit status
my $signer;
if (/\[GNUPG:\]\s+GOODSIG\s+\S+\s+(\S+)/) {
$ok = 0;
$signer = $1;
} elsif (/\[GNUPG:\]\s+NODATA/ || /\[GNUPG:\]\s+UNEXPECTED/) {
$ok = 1;
} elsif (/\[GNUPG:\]\s+NO_PUBKEY/) {
$ok = 2;
} elsif (/\[GNUPG:\]\s+BADSIG\s+/) {
$ok = 3;
}
return ($ok, $signer);
close $input;
waitpid ($pid, 0);
unlink ($filename, "$filename.asc");
umask $umask;
return ($ok, $signer || '');
}
# Log an error message, attempting syslog first based on $syslog_method
@ -519,7 +571,6 @@ sub errmsg {
}
sub fail {
unlink($tmp);
&errmsg($_[0]);
exit 255;
}
@ -574,9 +625,9 @@ sub shlock {
return -1;
}
# either this process unlinked the lockfile because it was bogus,
# or between this process's link() and open() the other process
# holding the lock unlinked it. This process can now try to aquire.
# either this process unlinked the lockfile because it was bogus, or
# between this process's link() and open() the other process holding
# the lock unlinked it. This process can now try to aquire.
if (! link($ltmp, $file)) {
unlink($ltmp);
return $! == &EEXIST ? 0 : -1; # maybe another proc grabbed the lock
@ -664,6 +715,8 @@ invoking the B<pgp> or B<gpgv> program. It is the responsibility of the
person who installs B<pgpverify> to ensure that when B<pgp> or B<gpgv>
runs, it has the ability to locate and read a PGP key file that contains
the PGP public keys for the appropriate Usenet hierarchy administrators.
B<pgpverify> can be pointed to an appropriate key ring by editing
variables at the beginning of this script.
=head1 NOTES
@ -716,7 +769,8 @@ hierarchy administration.
=head1 HISTORY
B<pgpverify> was written by David C Lawrence <tale@isc.org>. Manual page
provided by James Ralston.
provided by James Ralston. It is currently maintained by Russ Allbery
<rra@stanford.edu>.
=head1 COPYRIGHT AND LICENSE