From: Jacob Bachmeyer Date: Sat, 12 Nov 2022 03:21:30 +0000 (-0600) Subject: Add verify_detached_signature X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=f34a38e3aa1bec8bd3b6c92e40d276a230c2b09d;p=gatekeeper.git Add verify_detached_signature --- diff --git a/gatekeeper.pl b/gatekeeper.pl index ac15d79..280068e 100755 --- a/gatekeeper.pl +++ b/gatekeeper.pl @@ -743,8 +743,12 @@ sub slurp_clearsigned_message { =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 is zero and C is not defined in the returned hashref. @@ -801,7 +805,7 @@ values are untainted. The C field, if present, is untainted. =cut -# helper for verify_clearsigned_message +# helper for verify_clearsigned_message and verify_detached_signature sub _analyze_gpgv_output { my $ret = shift; # hashref @@ -1051,6 +1055,141 @@ sub verify_clearsigned_message { 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; + +} + # # - Package configuration access