=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 = @_;
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;
}