Begin to implement new gpgv clearsigned message verification
authorJacob Bachmeyer <jcb@gnu.org>
Sun, 23 Oct 2022 03:55:33 +0000 (22:55 -0500)
committerJacob Bachmeyer <jcb@gnu.org>
Sun, 23 Oct 2022 03:55:33 +0000 (22:55 -0500)
The new code allows presenting gpgv with exactly the directive that was parsed
and eliminates all risk of confusion by keeping the output, log, and status
channels separate.  This also avoids using the shell to run gpgv.

The use of a dedicated pipe with --status-fd protects against CVE-2018-12020
and any similar future vulnerabilities.

gatekeeper.pl

index a4175e7c0d29d61794c4b503c8376d3faa624204..852334659e6aa542ed6b422ae9e1d664a92d5a63 100755 (executable)
@@ -168,6 +168,9 @@ use constant ();    # load this now for use in later BEGIN blocks
 
 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;
@@ -1288,6 +1291,216 @@ sub guess_uploader_email {
   }
 }
 
+=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