From: Jacob Bachmeyer Date: Sat, 12 Nov 2022 02:28:16 +0000 (-0600) Subject: Factor analysis out of verify_clearsigned_message X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=ac064914d17d72b2e9d5604028353af174c67bab;p=gatekeeper.git Factor analysis out of verify_clearsigned_message This is in preparation for also using --status-fd when verifying detached signatures for uploaded files. --- diff --git a/gatekeeper.pl b/gatekeeper.pl index e57e5fc..ac15d79 100755 --- a/gatekeeper.pl +++ b/gatekeeper.pl @@ -801,6 +801,102 @@ values are untainted. The C field, if present, is untainted. =cut +# helper for verify_clearsigned_message +sub _analyze_gpgv_output { + my $ret = shift; # hashref + + # CVE-2022-34903 caused GPG to dump a chunk of its heap to the status fd, + # and, eventually, segfault upon reaching unallocated address space. + # This had two recognizable consequences: + # - The GPG process dies with SIGSEGV. + # - The status output very likely contains multiple NUL bytes. + push @{$ret->{TILT}}, 'gpgv died on signal '.WTERMSIG($ret->{exitcode}) + if WIFSIGNALED($ret->{exitcode}); + for (qw(output log status)) + { push @{$ret->{TILT}}, "gpgv $_ contained NUL byte" + if $ret->{'raw_'.$_} =~ m/\0/ } + + local *_; + # counters + my $intro_status = 0; my $check_status = 0; my $verdict_status = 0; + + open my $status, '<', \($ret->{raw_status}) + or ftp_abort('open in-memory file for gpgv status'); + while (<$status>) { + chomp; + unless (m/^\[GNUPG:\] /g) { + push @{$ret->{TILT}}, "gpgv status line lacks required prefix"; + last; # stop parsing if an invalid line is found + } + + if (m/\GNEWSIG/gc) { + $intro_status++; # Note that NEWSIG is optional + } elsif (m/\G(GOOD|EXP|EXPKEY|REVKEY|BAD|ERR)SIG ([[:xdigit:]]+) /gc) { + # $1 -- result tag $2 -- long ID or fingerprint + # The next field is the primary username, except ERRSIG, but there is + # no guarantee that the primary UID will contain an email address. + if (length($2) > 16) { # We have a key fingerprint + $ret->{key_fingerprint} = $2; + $ret->{key_longid} = substr $2,-16; + } else { # We only have a long key ID + $ret->{key_longid} = $2; + } + + if ($1 eq 'BAD') { + $verdict_status++; + push @{$ret->{TILT}}, 'gpgv reported a bad signature, but exited zero' + if 0 == $ret->{exitcode}; + } elsif ($1 eq 'ERR') { # an ERRSIG line + $verdict_status++; + if (m/\G(\d+)\s(\d+)\s([[:xdigit:]]{2})\s([-:T[:digit:]Z+]+)\s(\d+) + /gcx) { + # $1 -- pubkey algorithm $2 -- digest algorithm + # $3 -- timestamp $4 -- result code + ftp_abort('gpgv returned an ISO8601 timestamp; implementation needed') + if $3 =~ m/T/; + $ret->{sig_creation} = $3; + } else + { push @{$ret->{TILT}}, 'gpgv ERRSIG line failed parsing' } + + push @{$ret->{TILT}}, 'gpgv reported an error, but exited zero' + if 0 == $ret->{exitcode}; + } else { # GOODSIG/EXPSIG/EXPKEYSIG/REVKEYSIG + $check_status++; + } + } elsif (m/\G(VALID)SIG\s([[:xdigit:]]+)\s(\d{4}-\d{2}-\d{2})\s + ([-:T[:digit:]Z+]+)\s([-:T[:digit:]Z+]+)\s(\d+)\s(\S+)\s + (\d+)\s(\d+)\s([[:xdigit:]]{2})\s([[:xdigit:]]+) + /gcx) { + $verdict_status++; + # $1 -- valid tag $2 -- key fingerprint + # $3 -- signature date $4 -- signature timestamp + # $5 -- expiration timestamp $6 -- signature version + # $7 -- reserved $8 -- pubkey algorithm + # $9 -- digest algorithm $10 -- signature class + # $11 -- primary key fingerprint + $ret->{key_fingerprint} = $2; + $ret->{key_longid} = substr $2,-16; + ftp_abort('gpgv returned an ISO8601 timestamp; implementation needed') + if $4 =~ m/T/ || $5 =~ m/T/; + $ret->{sig_creation} = $4; + # GPG reports 0 if the signature does not expire + $ret->{sig_expiration} = $5 if $5 > 0; + } + } + close $status or ftp_abort('close in-memory file for gpgv status'); + + push @{$ret->{TILT}}, 'gpgv reported more than one signature' + if $intro_status > 1; + push @{$ret->{TILT}}, 'gpgv reported more than one signature check' + if $check_status > 1; + push @{$ret->{TILT}}, 'gpgv reported more than one signature verdict' + if $verdict_status > 1; + push @{$ret->{TILT}}, 'gpgv reported no signature verdict at all' + if $verdict_status < 1; + + return $ret; +} + sub verify_clearsigned_message { my $text = shift; my @keyrings = @_; @@ -950,96 +1046,7 @@ sub verify_clearsigned_message { my %ret = (exitcode => $?, raw_output => $raw_output, raw_log => $raw_log, raw_status => $raw_status); - # Analyze the results - - # CVE-2022-34903 caused GPG to dump a chunk of its heap to the status fd, - # and, eventually, segfault upon reaching unallocated address space. - # This had two recognizable consequences: - # - The GPG process dies with SIGSEGV. - # - The status output very likely contains multiple NUL bytes. - push @{$ret{TILT}}, 'gpgv died on signal '.WTERMSIG($ret{exitcode}) - if WIFSIGNALED($ret{exitcode}); - for (qw(output log status)) - { push @{$ret{TILT}}, "gpgv $_ contained NUL byte" - if $ret{'raw_'.$_} =~ m/\0/ } - - local *_; - # counters - my $intro_status = 0; my $check_status = 0; my $verdict_status = 0; - - open my $status, '<', \$ret{raw_status} - or ftp_abort('open in-memory file for gpgv status'); - while (<$status>) { - chomp; - unless (m/^\[GNUPG:\] /g) { - push @{$ret{TILT}}, "gpgv status line lacks required prefix"; - last; # stop parsing if an invalid line is found - } - - if (m/\GNEWSIG/gc) { - $intro_status++; # Note that NEWSIG is optional - } elsif (m/\G(GOOD|EXP|EXPKEY|REVKEY|BAD|ERR)SIG ([[:xdigit:]]+) /gc) { - # $1 -- result tag $2 -- long ID or fingerprint - # The next field is the primary username, except ERRSIG, but there is - # no guarantee that the primary UID will contain an email address. - if (length($2) > 16) { # We have a key fingerprint - $ret{key_fingerprint} = $2; - $ret{key_longid} = substr $2,-16; - } else { # We only have a long key ID - $ret{key_longid} = $2; - } - - if ($1 eq 'BAD') { - $verdict_status++; - push @{$ret{TILT}}, 'gpgv reported a bad signature, but exited zero' - if 0 == $ret{exitcode}; - } elsif ($1 eq 'ERR') { # an ERRSIG line - $verdict_status++; - if (m/\G(\d+)\s(\d+)\s([[:xdigit:]]{2})\s([-:T[:digit:]Z+]+)\s(\d+) - /gcx) { - # $1 -- pubkey algorithm $2 -- digest algorithm - # $3 -- timestamp $4 -- result code - ftp_abort('gpgv returned an ISO8601 timestamp; implementation needed') - if $3 =~ m/T/; - $ret{sig_creation} = $3; - } else - { push @{$ret{TILT}}, 'gpgv ERRSIG line failed parsing' } - - push @{$ret{TILT}}, 'gpgv reported an error, but exited zero' - if 0 == $ret{exitcode}; - } else { # GOODSIG/EXPSIG/EXPKEYSIG/REVKEYSIG - $check_status++; - } - } elsif (m/\G(VALID)SIG\s([[:xdigit:]]+)\s(\d{4}-\d{2}-\d{2})\s - ([-:T[:digit:]Z+]+)\s([-:T[:digit:]Z+]+)\s(\d+)\s(\S+)\s - (\d+)\s(\d+)\s([[:xdigit:]]{2})\s([[:xdigit:]]+) - /gcx) { - $verdict_status++; - # $1 -- valid tag $2 -- key fingerprint - # $3 -- signature date $4 -- signature timestamp - # $5 -- expiration timestamp $6 -- signature version - # $7 -- reserved $8 -- pubkey algorithm - # $9 -- digest algorithm $10 -- signature class - # $11 -- primary key fingerprint - $ret{key_fingerprint} = $2; - $ret{key_longid} = substr $2,-16; - ftp_abort('gpgv returned an ISO8601 timestamp; implementation needed') - if $4 =~ m/T/ || $5 =~ m/T/; - $ret{sig_creation} = $4; - # GPG reports 0 if the signature does not expire - $ret{sig_expiration} = $5 if $5 > 0; - } - } - close $status or ftp_abort('close in-memory file for gpgv status'); - - push @{$ret{TILT}}, 'gpgv reported more than one signature' - if $intro_status > 1; - push @{$ret{TILT}}, 'gpgv reported more than one signature check' - if $check_status > 1; - push @{$ret{TILT}}, 'gpgv reported more than one signature verdict' - if $verdict_status > 1; - push @{$ret{TILT}}, 'gpgv reported no signature verdict at all' - if $verdict_status < 1; + _analyze_gpgv_output(\%ret); return \%ret; }