This is an automated email from the git hooks/post-receive script. guillem pushed a commit to branch main in repository dpkg.
View the commit online: https://git.dpkg.org/cgit/dpkg/dpkg.git/commit/?id=4af4a46fa12221e8e7ea7d525619a2dbc89f7786 commit 4af4a46fa12221e8e7ea7d525619a2dbc89f7786 Author: Guillem Jover <[email protected]> AuthorDate: Fri Jul 29 23:25:39 2022 +0200 Dpkg::OpenPGP::Backend: Refactor GnuPG functions from Dpkg::OpenPGP Create a generic Dpkg::OpenPGP::Backend as the base class for all backend implementations. Move the GnuPG specific backend into a new Dpkg::OpenPGP::Backend::GnuPG. This will make adding other implementations easier. --- scripts/Dpkg/OpenPGP.pm | 227 ++------------------- scripts/Dpkg/OpenPGP/Backend.pm | 109 ++++++++++ .../Dpkg/{OpenPGP.pm => OpenPGP/Backend/GnuPG.pm} | 61 ++---- scripts/Makefile.am | 2 + 4 files changed, 141 insertions(+), 258 deletions(-) diff --git a/scripts/Dpkg/OpenPGP.pm b/scripts/Dpkg/OpenPGP.pm index 005d6d7eb..06286d61d 100644 --- a/scripts/Dpkg/OpenPGP.pm +++ b/scripts/Dpkg/OpenPGP.pm @@ -18,61 +18,43 @@ package Dpkg::OpenPGP; use strict; use warnings; -use POSIX qw(:sys_wait_h); -use File::Temp; -use MIME::Base64; - use Dpkg::Gettext; use Dpkg::ErrorHandling; use Dpkg::IPC; -use Dpkg::File; use Dpkg::Path qw(find_command); -use Dpkg::OpenPGP::ErrorCodes; +use Dpkg::OpenPGP::Backend::GnuPG; our $VERSION = '0.01'; -sub _detect_cmd { - my ($cmd, $default) = @_; - - if (! defined $cmd || $cmd eq 'auto') { - return find_command($default); - } else { - return find_command($cmd); - } -} - sub new { my ($this, %opts) = @_; my $class = ref($this) || $this; - my $self = { - cmdv => _detect_cmd($opts{cmdv}, 'gpgv'), - cmd => _detect_cmd($opts{cmd}, 'gpg'), - }; + my $self = {}; bless $self, $class; - return $self; -} + my %backend_opts = ( + cmdv => $opts{cmdv} // 'auto', + cmd => $opts{cmd} // 'auto', + ); -sub _gpg_has_keystore { - my $self = shift; + $self->{backend} = Dpkg::OpenPGP::Backend::GnuPG->new(%backend_opts); - return 1 if ($ENV{GNUPGHOME} && -e $ENV{GNUPGHOME}) || - ($ENV{HOME} && -e "$ENV{HOME}/.gnupg"); - return 0; + return $self; } sub can_use_secrets { my ($self, $key) = @_; - return 0 unless $self->{cmd}; + return 0 unless $self->{backend}->has_backend_cmd(); + if ($key->type eq 'keyfile') { return 1 if -f $key->handle; } elsif ($key->type eq 'keystore') { return 1 if -e $key->handle; } else { # For IDs we need a keystore. - return $self->_gpg_has_keystore(); + return $self->{backend}->has_keystore(); } return 0; } @@ -80,212 +62,37 @@ sub can_use_secrets { sub get_trusted_keyrings { my $self = shift; - my @keyrings; - if (length $ENV{HOME} and -r "$ENV{HOME}/.gnupg/trustedkeys.gpg") { - push @keyrings, "$ENV{HOME}/.gnupg/trustedkeys.gpg"; - } - return @keyrings; -} - -# _pgp_* functions are strictly for applying or removing ASCII armor. -# See <https://datatracker.ietf.org/doc/html/rfc4880#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_dearmor_data { - my ($type, $data) = @_; - - # Note that we ignore an incorrect or absent checksum, following the - # guidance of - # <https://datatracker.ietf.org/doc/draft-ietf-openpgp-crypto-refresh/>. - my $armor_regex = qr{ - -----BEGIN\ PGP\ \Q$type\E-----[\r\t ]*\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; - - if ($data =~ m/$armor_regex/) { - return decode_base64($1); - } - return; -} - -sub _pgp_armor_checksum { - my ($data) = @_; - - # From the upcoming revision to RFC 4880 - # <https://datatracker.ietf.org/doc/draft-ietf-openpgp-crypto-refresh/>. - # - # 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) { - # Clear bit 25 to avoid overflow. - $crc &= 0xffffff; - $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; + return $self->{backend}->get_trusted_keyrings(); } sub armor { my ($self, $type, $in, $out) = @_; - my $raw_data = file_slurp($in); - my $data = _pgp_dearmor_data($type, $raw_data) // $raw_data; - my $armor = _pgp_armor_data($type, $data); - return OPENPGP_BAD_DATA unless defined $armor; - file_dump($out, $armor); - - return OPENPGP_OK; + return $self->{backend}->armor($type, $in, $out); } sub dearmor { my ($self, $type, $in, $out) = @_; - my $armor = file_slurp($in); - my $data = _pgp_dearmor_data($type, $armor); - return OPENPGP_BAD_DATA unless defined $data; - file_dump($out, $data); - - return OPENPGP_OK; -} - -sub _gpg_exec -{ - my ($self, @exec) = @_; - - my ($stdout, $stderr); - spawn(exec => \@exec, wait_child => 1, nocheck => 1, timeout => 10, - to_string => \$stdout, error_to_string => \$stderr); - if (WIFEXITED($?)) { - my $status = WEXITSTATUS($?); - print { *STDERR } "$stdout$stderr" if $status; - return $status; - } else { - subprocerr("@exec"); - } -} - -sub _gpg_options_weak_digests { - my @gpg_weak_digests = map { - (qw(--weak-digest), $_) - } qw(SHA1 RIPEMD160); - - return @gpg_weak_digests; -} - -sub _gpg_verify { - my ($self, $signeddata, $sig, $data, @certs) = @_; - - return OPENPGP_MISSING_CMD if ! $self->{cmdv} || ! $self->{cmd}; - - my $gpg_home = File::Temp->newdir('dpkg-gpg-verify.XXXXXXXX', TMPDIR => 1); - - my @exec; - if ($self->{cmdv}) { - push @exec, $self->{cmdv}; - } else { - push @exec, $self->{cmd}; - push @exec, qw(--no-options --no-default-keyring --batch --quiet); - } - push @exec, _gpg_options_weak_digests(); - push @exec, '--homedir', $gpg_home; - foreach my $cert (@certs) { - my $certring = File::Temp->new(UNLINK => 1, SUFFIX => '.pgp'); - my $rc = $self->dearmor('PUBLIC KEY BLOCK', $cert, $certring); - $certring = $cert if $rc; - push @exec, '--keyring', $certring; - } - push @exec, '--output', $data if defined $data; - if (! $self->{cmdv}) { - push @exec, '--verify'; - } - push @exec, $sig if defined $sig; - push @exec, $signeddata; - - my $status = $self->_gpg_exec(@exec); - return OPENPGP_NO_SIG if $status; - return OPENPGP_OK; + return $self->{backend}->dearmor($type, $in, $out); } sub inline_verify { my ($self, $inlinesigned, $data, @certs) = @_; - return $self->_gpg_verify($inlinesigned, undef, $data, @certs); + return $self->{backend}->inline_verify($inlinesigned, $data, @certs); } sub verify { my ($self, $data, $sig, @certs) = @_; - return $self->_gpg_verify($data, $sig, undef, @certs); -} - -sub _gpg_inline_sign { - my ($self, $data, $inlinesigned, $key) = @_; - - return OPENPGP_MISSING_CMD if ! $self->{cmd}; - - my @exec = ($self->{cmd}); - push @exec, _gpg_options_weak_digests(); - push @exec, qw(--utf8-strings --textmode --armor); - if ($key->type eq 'keyfile') { - # Promote the keyfile keyhandle to a keystore, this way we share the - # same gpg-agent and can get any password cached. - my $gpg_home = File::Temp->newdir('dpkg-sign.XXXXXXXX', TMPDIR => 1); - - push @exec, '--homedir', $gpg_home; - $self->_gpg_exec(@exec, qw(--quiet --no-tty --batch --import), $key->handle); - $key->set('keystore', $gpg_home); - } elsif ($key->type eq 'keystore') { - push @exec, '--homedir', $key->handle; - } else { - push @exec, '--local-user', $key->handle; - } - push @exec, '--output', $inlinesigned; - - my $rc = $self->_gpg_exec(@exec, '--clearsign', $data); - return OPENPGP_KEY_CANNOT_SIGN if $rc; - return OPENPGP_OK; + return $self->{backend}->verify($data, $sig, @certs); } sub inline_sign { my ($self, $data, $inlinesigned, $key) = @_; - return $self->_gpg_inline_sign($data, $inlinesigned, $key); + return $self->{backend}->inline_sign($data, $inlinesigned, $key); } 1; diff --git a/scripts/Dpkg/OpenPGP/Backend.pm b/scripts/Dpkg/OpenPGP/Backend.pm new file mode 100644 index 000000000..39d1cf475 --- /dev/null +++ b/scripts/Dpkg/OpenPGP/Backend.pm @@ -0,0 +1,109 @@ +# Copyright © 2017, 2022 Guillem Jover <[email protected]> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +package Dpkg::OpenPGP::Backend; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +use List::Util qw(first); + +use Dpkg::Path qw(find_command); +use Dpkg::OpenPGP::ErrorCodes; + +sub DEFAULT_CMDV { + return []; +} + +sub DEFAULT_CMD { + return []; +} + +sub _detect_cmd { + my ($cmd, $default) = @_; + + if (! defined $cmd || $cmd eq 'auto') { + return first { find_command($_) } @{$default}; + } else { + return find_command($cmd); + } +} + +sub new { + my ($this, %opts) = @_; + my $class = ref($this) || $this; + + my $self = { + strict_verify => $opts{strict_verify} // 1, + }; + bless $self, $class; + + $self->{cmdv} = _detect_cmd($opts{cmdv}, $self->DEFAULT_CMDV()); + $self->{cmd} = _detect_cmd($opts{cmd}, $self->DEFAULT_CMD()); + + return $self; +} + +sub has_backend_cmd { + my $self = shift; + + return defined $self->{cmd}; +} + +sub has_keystore { + my $self = shift; + + return 0; +} + +sub get_trusted_keyrings { + my $self = shift; + + return (); +} + +sub armor { + my ($self, $type, $in, $out) = @_; + + return OPENPGP_UNSUPPORTED_SUBCMD; +} + +sub dearmor { + my ($self, $type, $in, $out) = @_; + + return OPENPGP_UNSUPPORTED_SUBCMD; +} + +sub inline_verify { + my ($self, $inlinesigned, $data, @certs) = @_; + + return OPENPGP_UNSUPPORTED_SUBCMD; +} + +sub verify { + my ($self, $data, $sig, @certs) = @_; + + return OPENPGP_UNSUPPORTED_SUBCMD; +} + +sub inline_sign { + my ($self, $data, $inlinesigned, $key) = @_; + + return OPENPGP_UNSUPPORTED_SUBCMD; +} + +1; diff --git a/scripts/Dpkg/OpenPGP.pm b/scripts/Dpkg/OpenPGP/Backend/GnuPG.pm similarity index 85% copy from scripts/Dpkg/OpenPGP.pm copy to scripts/Dpkg/OpenPGP/Backend/GnuPG.pm index 005d6d7eb..e60826575 100644 --- a/scripts/Dpkg/OpenPGP.pm +++ b/scripts/Dpkg/OpenPGP/Backend/GnuPG.pm @@ -1,4 +1,4 @@ -# Copyright © 2017 Guillem Jover <[email protected]> +# Copyright © 207, 2022 Guillem Jover <[email protected]> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,48 +13,34 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. -package Dpkg::OpenPGP; +package Dpkg::OpenPGP::Backend::GnuPG; use strict; use warnings; +our $VERSION = '0.01'; + use POSIX qw(:sys_wait_h); use File::Temp; use MIME::Base64; -use Dpkg::Gettext; use Dpkg::ErrorHandling; use Dpkg::IPC; use Dpkg::File; use Dpkg::Path qw(find_command); use Dpkg::OpenPGP::ErrorCodes; -our $VERSION = '0.01'; - -sub _detect_cmd { - my ($cmd, $default) = @_; +use parent qw(Dpkg::OpenPGP::Backend); - if (! defined $cmd || $cmd eq 'auto') { - return find_command($default); - } else { - return find_command($cmd); - } +sub DEFAULT_CMDV { + return [ qw(gpgv) ]; } -sub new { - my ($this, %opts) = @_; - my $class = ref($this) || $this; - - my $self = { - cmdv => _detect_cmd($opts{cmdv}, 'gpgv'), - cmd => _detect_cmd($opts{cmd}, 'gpg'), - }; - bless $self, $class; - - return $self; +sub DEFAULT_CMD { + return [ qw(gpg) ]; } -sub _gpg_has_keystore { +sub has_keystore { my $self = shift; return 1 if ($ENV{GNUPGHOME} && -e $ENV{GNUPGHOME}) || @@ -62,21 +48,6 @@ sub _gpg_has_keystore { return 0; } -sub can_use_secrets { - my ($self, $key) = @_; - - return 0 unless $self->{cmd}; - if ($key->type eq 'keyfile') { - return 1 if -f $key->handle; - } elsif ($key->type eq 'keystore') { - return 1 if -e $key->handle; - } else { - # For IDs we need a keystore. - return $self->_gpg_has_keystore(); - } - return 0; -} - sub get_trusted_keyrings { my $self = shift; @@ -237,8 +208,8 @@ sub _gpg_verify { push @exec, $sig if defined $sig; push @exec, $signeddata; - my $status = $self->_gpg_exec(@exec); - return OPENPGP_NO_SIG if $status; + my $rc = $self->_gpg_exec(@exec); + return OPENPGP_NO_SIG if $rc; return OPENPGP_OK; } @@ -254,7 +225,7 @@ sub verify { return $self->_gpg_verify($data, $sig, undef, @certs); } -sub _gpg_inline_sign { +sub inline_sign { my ($self, $data, $inlinesigned, $key) = @_; return OPENPGP_MISSING_CMD if ! $self->{cmd}; @@ -282,10 +253,4 @@ sub _gpg_inline_sign { return OPENPGP_OK; } -sub inline_sign { - my ($self, $data, $inlinesigned, $key) = @_; - - return $self->_gpg_inline_sign($data, $inlinesigned, $key); -} - 1; diff --git a/scripts/Makefile.am b/scripts/Makefile.am index a6364f677..4375e23f7 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -106,6 +106,8 @@ nobase_dist_perllib_DATA = \ Dpkg/IPC.pm \ Dpkg/Lock.pm \ Dpkg/OpenPGP.pm \ + Dpkg/OpenPGP/Backend.pm \ + Dpkg/OpenPGP/Backend/GnuPG.pm \ Dpkg/OpenPGP/ErrorCodes.pm \ Dpkg/OpenPGP/KeyHandle.pm \ Dpkg/Package.pm \ -- Dpkg.Org's dpkg

