Add verify_detached_signature
authorJacob Bachmeyer <jcb@gnu.org>
Sat, 12 Nov 2022 03:21:30 +0000 (21:21 -0600)
committerJacob Bachmeyer <jcb@gnu.org>
Sat, 12 Nov 2022 03:21:30 +0000 (21:21 -0600)
gatekeeper.pl

index ac15d79ab66b07a9d6f285927fe5b101dc383b23..280068e993e91c7e702f5ab5aea44614749360b1 100755 (executable)
@@ -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<exitcode> is zero
 and C<TILT> is not defined in the returned hashref.
@@ -801,7 +805,7 @@ values are untainted.  The C<TILT> 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;
+
+}
+
 \f
 #
 # - Package configuration access