=item $results = verify_clearsigned_message ( $text, @keyrings )
-Verify the PGP-clearsigned message in TEXT, using a key from KEYRINGS. The
-TEXT may be tainted, but the list of KEYRINGS must be untainted.
+=item $results = verify_detached_signature ( $file, $sigfile, @keyrings )
+
+Verify the PGP-clearsigned message in TEXT or the detached signature in
+SIGFILE for FILE, using a key from KEYRINGS. The TEXT may be tainted, but
+the list of KEYRINGS and the FILE and SIGFILE values must be
+untainted.
The message signature should be considered verified iff C<exitcode> is zero
and C<TILT> is not defined in the returned hashref.
=cut
-# helper for verify_clearsigned_message
+# helper for verify_clearsigned_message and verify_detached_signature
sub _analyze_gpgv_output {
my $ret = shift; # hashref
return \%ret;
}
+sub verify_detached_signature {
+ my $filename = shift;
+ my $sigfilename = shift;
+ my @keyrings = @_;
+
+ # This is very similar to verify_clearsigned_message, but slightly
+ # simpler because all input to GPG is supplied from files, so we do not
+ # have a pipe to the child process. We still need the other pipes and we
+ # still have the same risks of exploits against GPG.
+
+ {
+ my $file_size = -s $filename;
+ my $sig_file_size = -s $sigfilename;
+
+ ftp_syslog('debug', "DEBUG: $sigfilename size is $sig_file_size")
+ if DEBUG;
+ ftp_syslog('debug', "DEBUG: $filename size is $file_size")
+ if DEBUG;
+ }
+
+ 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');
+
+ 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, $sigfilename, $filename;
+
+ 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_output; close $gpgv_log;
+ close $gpgv_status; close $gpgv_flag;
+
+ our $AbortPipe = $gpgv_flag_sink; # pipe to parent
+ our $AbortExitCode = 120; # arbitrary 7-bit exit code
+ # no need to use local here; this process will either exec or abort
+
+ # Adjust close-on-exec flags:
+ my $flags;
+ # - clear on status and log sinks
+ $flags = fcntl $gpgv_status_sink, F_GETFD, 0
+ or ftp_abort("ERR: fcntl F_GETFD on status: $!");
+ fcntl $gpgv_status_sink, F_SETFD, $flags & ~FD_CLOEXEC
+ or ftp_abort("ERR: fcntl F_SETFD on status: $!");
+ $flags = fcntl $gpgv_log_sink, F_GETFD, 0
+ or ftp_abort("ERR: fcntl F_GETFD on log: $!");
+ fcntl $gpgv_log_sink, F_SETFD, $flags & ~FD_CLOEXEC
+ or ftp_abort("ERR: fcntl F_SETFD on log: $!");
+ # - set on flag pipe sink
+ $flags = fcntl $gpgv_flag_sink, F_GETFD, 0
+ or ftp_abort("ERR: fcntl F_GETFD on flag: $!");
+ fcntl $gpgv_flag_sink, F_SETFD, $flags | FD_CLOEXEC
+ or ftp_abort("ERR: fcntl F_SETFD on flag: $!");
+
+ # Prepare STDOUT/STDERR
+ open STDOUT, '>&', $gpgv_output_sink or ftp_abort("ERR: set stdout: $!");
+ open STDERR, '>&', $gpgv_output_sink or ftp_abort("ERR: set stderr: $!");
+
+ # Exec gpgv
+ exec { GPGV_BIN } @gpgv_args or ftp_abort("ERR: $!");
+ }
+
+ # The parent continues here...
+ 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
+ ftp_abort
+ ("gpg verify of directive file failed (error executing gpgv): $1");
+ }
+ }
+ close $gpgv_flag; # child has closed its end one way or another
+
+ foreach my $cell ([$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]: $!");
+ }
+
+ my $Rchk = '';
+ vec($Rchk, (fileno $_), 1) = 1 for ($gpgv_output, $gpgv_log, $gpgv_status);
+ my $Rrdy = '';
+ my $raw_output = ''; my $raw_log = ''; my $raw_status = '';
+ 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;
+ }
+ }
+
+ select $Rrdy=$Rchk, undef, 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_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);
+
+ _analyze_gpgv_output(\%ret);
+
+ return \%ret;
+
+}
+
\f
#
# - Package configuration access