use Carp qw(cluck); # for stack trace on error in ftp_syslog
+use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC
+ F_GETFL F_SETFL O_NONBLOCK);
+
use FindBin;
use File::Spec;
use Pod::Usage;
}
}
+=item $results = verify_clearsigned_message ( $text, @keyrings )
+
+Verify the PGP-clearsigned message in TEXT, using a key from KEYRINGS.
+
+The return value is a hashref containing:
+
+=over
+
+=item exitcode
+
+The exit status from gpgv. This will be zero if gpgv considers the
+signature valid.
+
+=item raw_output
+
+=item raw_log
+
+=item raw_status
+
+The complete collected output, log, and status buffers.
+
+=item key_longid
+
+The 64-bit long key ID of the key that signed TEXT, if available.
+
+=item key_fingerprint
+
+The fingerprint of the PGP key that signed TEXT, if available.
+
+=item ...
+
+
+=back
+
+=cut
+
+sub verify_clearsigned_message {
+ my $text = shift;
+ my @keyrings = @_;
+
+ ftp_syslog('debug', 'DEBUG: message size is '.length($text)) if DEBUG;
+
+ # We need a few pipes:
+ # - clearsigned message to gpgv stdin
+ # - output from gpgv stdout/stderr
+ # - log from gpgv --logger-fd
+ # - status from gpgv --status-fd
+ # - a flag pipe to indicate successful exec or carry an error
+
+ # The three output streams from gpgv must be kept separate, or
+ # CVE-2018-12020 "SigSpoof" issues can occur. Worse, the gpgv status
+ # output must be examined with care, as there has been at least one bug
+ # (CVE-2022-34903) whereby gpgv could be tricked to emit arbitrary output
+ # on the status pipe.
+ pipe my $gpgv_stdin, my $gpgv_stdin_source
+ or ftp_abort('failed to create pipe for gpgv stdin');
+ pipe my $gpgv_output, my $gpgv_output_sink
+ or ftp_abort('failed to create pipe for gpgv output');
+ pipe my $gpgv_log, my $gpgv_log_sink
+ or ftp_abort('failed to create pipe for gpgv log');
+ pipe my $gpgv_status, my $gpgv_status_sink
+ or ftp_abort('failed to create pipe for gpgv status');
+ pipe my $gpgv_flag, my $gpgv_flag_sink
+ or ftp_abort('failed to create pipe for gpgv flag');
+
+ # ensure autoflush on writes to gpgv
+ { my $outhandle = select $gpgv_stdin_source; $| = 1; select $outhandle }
+
+ my @gpgv_args = ( GPGV_BIN,
+ '--logger-fd', fileno $gpgv_log_sink,
+ '--status-fd', fileno $gpgv_status_sink );
+ push @gpgv_args, '--keyring', $_ for @keyrings;
+ push @gpgv_args, '-';
+
+ ftp_syslog('debug', 'DEBUG: gpgv command line: '.join(' ', @gpgv_args))
+ if DEBUG;
+
+ my $pid = fork;
+ ftp_abort('failed to fork child for gpgv')
+ unless defined $pid;
+
+ unless ($pid) {
+ # We are in the child process...
+ close $gpgv_stdin_source;
+ close $gpgv_output; close $gpgv_log;
+ close $gpgv_status; close $gpgv_flag;
+
+ # Adjust close-on-exec flags:
+ my $flags;
+ # - clear on status and log sinks
+ unless ($flags = fcntl $gpgv_status_sink, F_GETFD, 0) {
+ print $gpgv_flag_sink "ERR: fcntl F_GETFD on status: $!\n";
+ exit 120; # arbitrary 7.bit exit code
+ }
+ unless (fcntl $gpgv_status_sink, F_SETFD, $flags & ~FD_CLOEXEC) {
+ print $gpgv_flag_sink "ERR: fcntl F_SETFD on status: $!\n";
+ exit 120; # arbitrary 7.bit exit code
+ }
+ unless ($flags = fcntl $gpgv_log_sink, F_GETFD, 0) {
+ print $gpgv_flag_sink "ERR: fcntl F_GETFD on log: $!\n";
+ exit 120; # arbitrary 7.bit exit code
+ }
+ unless (fcntl $gpgv_log_sink, F_SETFD, $flags & ~FD_CLOEXEC) {
+ print $gpgv_flag_sink "ERR: fcntl F_SETFD on log: $!\n";
+ exit 120; # arbitrary 7.bit exit code
+ }
+ # - set on flag pipe sink
+ unless ($flags = fcntl $gpgv_flag_sink, F_GETFD, 0) {
+ print $gpgv_flag_sink "ERR: fcntl F_GETFD on flag: $!\n";
+ exit 120; # arbitrary 7.bit exit code
+ }
+ unless (fcntl $gpgv_flag_sink, F_SETFD, $flags | FD_CLOEXEC) {
+ print $gpgv_flag_sink "ERR: fcntl F_SETFD on flag: $!\n";
+ exit 120; # arbitrary 7.bit exit code
+ }
+
+ # Prepare STDIN/STDOUT/STDERR
+ unless (open STDIN, '<&', $gpgv_stdin) {
+ print $gpgv_flag_sink "ERR: set stdin: $!\n";
+ exit 120; # arbitrary 7-bit exit code
+ }
+ unless (open STDOUT, '>&', $gpgv_output_sink) {
+ print $gpgv_flag_sink "ERR: set stdout: $!\n";
+ exit 120; # arbitrary 7-bit exit code
+ }
+ unless (open STDERR, '>&', $gpgv_output_sink) {
+ print $gpgv_flag_sink "ERR: set stderr: $!\n";
+ exit 120; # arbitrary 7-bit exit code
+ }
+
+ # Exec gpgv
+ exec { GPGV_BIN } @gpgv_args
+ or print $gpgv_flag_sink "ERR: $!\n";
+ exit 120; # arbitrary 7-bit exit code
+ }
+
+ # The parent continues here...
+ close $gpgv_stdin;
+ close $gpgv_output_sink; close $gpgv_log_sink;
+ close $gpgv_status_sink; close $gpgv_flag_sink;
+
+ # This is a bit tricky: we need to know if gpgv could not be run, so we
+ # have an extra pipe that will either report an error or be closed if the
+ # exec succeeds in the child process.
+ while (defined(my $err = <$gpgv_flag>)) {
+ chomp $err;
+ if ($err =~ m/^ERR: (.*)$/) {
+ # This is bad - we couldn't even execute the gpgv command properly
+ guess_uploader_email($text);
+ fatal("gpg verify of directive file failed (error executing gpgv): $1",
+ 0,'',2);
+ }
+ }
+ close $gpgv_flag; # child has closed its end one way or another
+
+ foreach my $cell ([$gpgv_stdin_source, 'message'], [$gpgv_output, 'output'],
+ [$gpgv_log, 'log'], [$gpgv_status, 'status']) {
+ my $flags = fcntl $cell->[0], F_GETFL, 0
+ or ftp_abort("gpgv: fcntl F_GETFL $cell->[1]: $!");
+ fcntl $cell->[0], F_SETFL, $flags | O_NONBLOCK
+ or ftp_abort("gpgv: fcntl F_SETFL $cell->[1]: $!");
+ }
+
+ local $SIG{PIPE} = sub { ftp_abort('gpgv exited unexpectedly') };
+ my $Rchk = ''; my $Wchk = '';
+ vec($Wchk, (fileno $gpgv_stdin_source), 1) = 1;
+ vec($Rchk, (fileno $_), 1) = 1 for ($gpgv_output, $gpgv_log, $gpgv_status);
+ my $Rrdy = ''; my $Wrdy = '';
+ my $raw_output = ''; my $raw_log = ''; my $raw_status = '';
+ pos $text = 0; # use this slot to store a position because we can
+ do {
+ foreach my $cell ([$gpgv_output, \$raw_output], [$gpgv_log, \$raw_log],
+ [$gpgv_status, \$raw_status]) {
+ if (vec($Rrdy, (fileno $cell->[0]), 1)) {
+ my $eof; # defined and zero at eof
+ 1 while
+ $eof = sysread $cell->[0], ${$cell->[1]}, 128, length ${$cell->[1]};
+ vec($Rchk, (fileno $cell->[0]), 1) = 0 if defined $eof && $eof == 0;
+ }
+ }
+
+ if (defined fileno $gpgv_stdin_source
+ && vec($Wrdy, (fileno $gpgv_stdin_source), 1)) {
+ my $err = syswrite $gpgv_stdin_source, $text, 128, pos $text;
+ pos $text += $err if defined $err;
+ unless (pos $text < length $text) {
+ vec($Wchk, (fileno $gpgv_stdin_source), 1) = 0;
+ close $gpgv_stdin_source;
+ }
+ }
+
+ select $Rrdy=$Rchk, $Wrdy=$Wchk, undef, undef
+ if grep vec($Rchk, (fileno $_), 1),
+ $gpgv_output, $gpgv_log, $gpgv_status;
+ } while (grep vec($Rchk, (fileno $_), 1),
+ $gpgv_output, $gpgv_log, $gpgv_status);
+
+ close $gpgv_stdin_source; close $gpgv_output;
+ close $gpgv_log; close $gpgv_status;
+ waitpid $pid, 0; # reap child that ran gpgv
+
+ # Prepare the return structure
+ my %ret = (exitcode => $?, raw_output => $raw_output,
+ raw_log => $raw_log, raw_status => $raw_status);
+
+ # TODO: analyze results...
+
+ return \%ret;
+}
+
#
# Verify that the signature used for the directive file is valid for
# this package's keyring. We go through all keyring files, starting at the