package Devscripts::Uscan::Keyring; use strict; use Devscripts::Uscan::Output; use Devscripts::Uscan::Utils; use Dpkg::IPC; use Dpkg::Path qw/find_command/; use File::Copy qw/copy move/; use File::Path qw/make_path remove_tree/; use File::Temp qw/tempfile tempdir/; use List::Util qw/first/; use MIME::Base64; # _pgp_* functions are strictly for applying or removing ASCII armor. # see https://www.rfc-editor.org/rfc/rfc9580.html#section-6 for more # details. # Note that these _pgp_* functions are only necessary while relying on # gpgv, and gpgv itself does not verify multiple signatures correctly # (see https://bugs.debian.org/1010955) sub _pgp_unarmor_data { my ($type, $data, $filename) = @_; # note that we ignore an incorrect or absent checksum, following the # guidance of # https://www.rfc-editor.org/rfc/rfc9580.html#section-6.1-3 my $armor_regex = qr{ -----BEGIN\ PGP\ \Q$type\E-----[\r\t ]*\n (?:[^:\n]+:\ [^\n]*[\r\t ]*\n)* [\r\t ]*\n ([a-zA-Z0-9/+\n]+={0,2})[\r\t ]*\n (?:=[a-zA-Z0-9/+]{4}[\r\t ]*\n)? -----END\ PGP\ \Q$type\E----- }xm; my $blocks = 0; my $binary; while ($data =~ m/$armor_regex/g) { $binary .= decode_base64($1); $blocks++; } if ($blocks > 1) { uscan_warn "Found multiple concatenated ASCII Armor blocks in\n" . " $filename, which is not an interoperable construct.\n" . " See .\n" . " Please concatenate them into a single ASCII Armor block. For example:\n" . " sq keyring merge --overwrite --output $filename \\\n" . " $filename"; } return $binary; } sub _pgp_armor_checksum { my ($data) = @_; # from https://www.rfc-editor.org/rfc/rfc9580.html#section-6.1.1 # # #define CRC24_INIT 0xB704CEL # #define CRC24_GENERATOR 0x864CFBL # typedef unsigned long crc24; # crc24 crc_octets(unsigned char *octets, size_t len) # { # crc24 crc = CRC24_INIT; # int i; # while (len--) { # crc ^= (*octets++) << 16; # for (i = 0; i < 8; i++) { # crc <<= 1; # if (crc & 0x1000000) { # crc &= 0xffffff; /* Clear bit 25 to avoid overflow */ # crc ^= CRC24_GENERATOR; # } # } # } # return crc & 0xFFFFFFL; # } # # the resulting three-octet-wide value then gets base64-encoded into # four base64 ASCII characters. my $CRC24_INIT = 0xB704CE; my $CRC24_GENERATOR = 0x864CFB; my @bytes = unpack 'C*', $data; my $crc = $CRC24_INIT; for my $b (@bytes) { $crc ^= ($b << 16); for (1 .. 8) { $crc <<= 1; if ($crc & 0x1000000) { $crc &= 0xffffff; # Clear bit 25 to avoid overflow $crc ^= $CRC24_GENERATOR; } } } my $sum = pack('CCC', (($crc >> 16) & 0xff, ($crc >> 8) & 0xff, $crc & 0xff)); return encode_base64($sum, q{}); } sub _pgp_armor_data { my ($type, $data) = @_; my $out = encode_base64($data, q{}) =~ s/(.{1,64})/$1\n/gr; chomp $out; my $crc = _pgp_armor_checksum($data); my $armor = <<~"ARMOR"; -----BEGIN PGP $type----- $out =$crc -----END PGP $type----- ARMOR return $armor; } sub new { my ($class) = @_; my $keyring; my $havegpgv = first { find_command($_) } qw(gpgv); my $havesopv = first { find_command($_) } qw(sopv); my $havesop = first { find_command($_) } qw(sqop rsop pgpainless-cli gosop); uscan_die("Please install a sopv variant.") unless (defined $havegpgv or defined $havesopv); # upstream/signing-key.pgp and upstream-signing-key.pgp are deprecated # but supported if (-r "debian/upstream/signing-key.asc") { $keyring = "debian/upstream/signing-key.asc"; } else { my $binkeyring = first { -r $_ } qw( debian/upstream/signing-key.pgp debian/upstream-signing-key.pgp ); if (defined $binkeyring) { make_path('debian/upstream', { mode => 0700, verbose => 'true' }); # convert to the policy complying armored key uscan_verbose( "Found upstream binary signing keyring: $binkeyring"); # Need to convert to an armored key $keyring = "debian/upstream/signing-key.asc"; uscan_warn "Found deprecated binary keyring ($binkeyring). " . "Please save it in armored format in $keyring. For example:\n" . " sop armor < $binkeyring > $keyring"; if ($havesop) { spawn( exec => [$havesop, 'armor'], from_file => $binkeyring, to_file => $keyring, wait_child => 1, ); } else { open my $inkeyring, '<', $binkeyring or uscan_warn( "Can't open $binkeyring to read deprecated binary keyring" ); read $inkeyring, my $keycontent, -s $inkeyring; close $inkeyring; open my $outkeyring, '>', $keyring or uscan_warn( "Can't open $keyring for writing ASCII-armored keyring"); my $outkey = _pgp_armor_data('PUBLIC KEY BLOCK', $keycontent); print $outkeyring $outkey or uscan_warn("Can't write ASCII-armored keyring to $keyring"); close $outkeyring or uscan_warn("Failed to close $keyring"); } uscan_warn("Generated upstream signing keyring: $keyring"); move $binkeyring, "$binkeyring.backup"; uscan_verbose( "Renamed upstream binary signing keyring: $binkeyring.backup"); } } # Need to convert an armored key to binary for use by gpgv if (defined $keyring) { uscan_verbose("Found upstream signing keyring: $keyring"); if ($keyring =~ m/\.asc$/ && !defined $havesopv) { # binary keyring is only necessary for gpgv: my $pgpworkdir = tempdir(CLEANUP => 1); my $newkeyring = "$pgpworkdir/upstream-signing-key.pgp"; open my $inkeyring, '<', $keyring or uscan_die("Can't open keyring file $keyring"); read $inkeyring, my $keycontent, -s $inkeyring; close $inkeyring; my $binkey = _pgp_unarmor_data('PUBLIC KEY BLOCK', $keycontent, $keyring); if ($binkey) { open my $outkeyring, '>:raw', $newkeyring or uscan_die("Can't write to temporary keyring $newkeyring"); print $outkeyring $binkey or uscan_die("Can't write $newkeyring"); close $outkeyring or uscan_die("Can't close $newkeyring"); $keyring = $newkeyring; } else { uscan_die("Failed to dearmor key(s) from $keyring"); } } } # Return undef if not key found else { return undef; } my $self = bless { keyring => $keyring, gpgv => $havegpgv, sopv => $havesopv, }, $class; return $self; } sub verify { my ($self, $sigfile, $newfile) = @_; uscan_verbose( "Verifying OpenPGP self signature of $newfile and extract $sigfile"); if ($self->{sopv}) { spawn( exec => [$self->{sopv}, 'inline-verify', $self->{keyring}], from_file => $newfile, to_file => $sigfile, wait_child => 1 ) or uscan_die("OpenPGP signature did not verify."); } else { unless ( uscan_exec_no_fail( $self->{gpgv}, '--homedir' => '/dev/null', '--keyring' => $self->{keyring}, '-o' => "$sigfile", "$newfile" ) >> 8 == 0 ) { uscan_die("OpenPGP signature did not verify."); } } } sub verifyv { my ($self, $sigfile, $base) = @_; uscan_verbose("Verifying OpenPGP signature $sigfile for $base"); if ($self->{sopv}) { spawn( exec => [$self->{sopv}, 'verify', $sigfile, $self->{keyring}], from_file => $base, wait_child => 1 ) or uscan_die("OpenPGP signature did not verify."); } else { unless ( uscan_exec_no_fail( $self->{gpgv}, '--homedir' => '/dev/null', '--keyring' => $self->{keyring}, $sigfile, $base ) >> 8 == 0 ) { uscan_die("OpenPGP signature did not verify."); } } } sub verify_git { my ($self, $gitdir, $tag, $git_upstream) = @_; my $commit; my @dir = $git_upstream ? () : ('--git-dir', $gitdir); spawn( exec => ['git', @dir, 'show-ref', $tag], to_string => \$commit ); uscan_die "git tag not found" unless ($commit); $commit =~ s/\s.*$//; chomp $commit; my $file; spawn( exec => ['git', @dir, 'cat-file', '-p', $commit], to_string => \$file ); my $dir; spawn(exec => ['mktemp', '-d'], to_string => \$dir); chomp $dir; unless ($file =~ /^(.*?\n)(\-+\s*BEGIN PGP SIGNATURE\s*\-+.*)$/s) { uscan_die "Tag $tag is not signed"; } open F, ">$dir/txt" or die $!; open S, ">$dir/sig" or die $!; print F $1; print S $2; close F; close S; if ($self->{sopv}) { spawn( exec => [$self->{sopv}, 'verify', "$dir/sig", $self->{keyring}], from_file => "$dir/txt", wait_child => 1 ) or uscan_die("OpenPGP signature did not verify"); } else { unless ( uscan_exec_no_fail( $self->{gpgv}, '--homedir' => '/dev/null', '--keyring' => $self->{keyring}, "$dir/sig", "$dir/txt" ) >> 8 == 0 ) { uscan_die("OpenPGP signature did not verify."); } } remove_tree($dir); } 1;