From e6d5d74d870188597ad3383091a75083fbf86518 Mon Sep 17 00:00:00 2001 From: Johannes 'josch' Schauer Date: Tue, 3 Dec 2019 10:16:39 +0100 Subject: [PATCH] --keyring now overwrites the default apt keyring - apt can only handle one directory and one file as keyring - the signed-by option is used to specify the keyrings for suites that are not known by apt --- coverage.sh | 72 ++++++++++++++++++++-- make_mirror.sh | 2 +- mmdebstrap | 159 ++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 203 insertions(+), 30 deletions(-) diff --git a/coverage.sh b/coverage.sh index 3498e39..ee70c7c 100755 --- a/coverage.sh +++ b/coverage.sh @@ -52,7 +52,7 @@ if [ ! -e shared/mmdebstrap ] || [ mmdebstrap -nt shared/mmdebstrap ]; then fi starttime= -total=112 +total=115 i=1 print_header() { @@ -969,20 +969,80 @@ cat << END > shared/test.sh #!/bin/sh set -eu export LC_ALL=C.UTF-8 -echo 'Acquire::Languages "none";' > config -$CMD --mode=root --variant=apt --aptopt='Acquire::Check-Valid-Until "false"' --keyring=/usr/share/keyrings/debian-archive-keyring.gpg --keyring=/usr/share/keyrings/ --aptopt=config $DEFAULT_DIST /tmp/debian-chroot $mirror -cat /tmp/debian-chroot/etc/apt/apt.conf.d/99mmdebstrap -printf 'Acquire::Check-Valid-Until "false";\nDir::Etc::Trusted "/usr/share/keyrings/debian-archive-keyring.gpg";\nDir::Etc::TrustedParts "/usr/share/keyrings/";\nAcquire::Languages "none";\n' | cmp /tmp/debian-chroot/etc/apt/apt.conf.d/99mmdebstrap - -rm /tmp/debian-chroot/etc/apt/apt.conf.d/99mmdebstrap +rm /etc/apt/trusted.gpg.d/*.gpg +$CMD --mode=root --variant=apt --keyring=/usr/share/keyrings/debian-archive-keyring.gpg --keyring=/usr/share/keyrings/ $DEFAULT_DIST /tmp/debian-chroot $mirror tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt - rm -r /tmp/debian-chroot END +if [ "$HAVE_QEMU" = "yes" ]; then + ./run_qemu.sh +else + echo "HAVE_QEMU != yes -- Skipping test..." +fi + +print_header "mode=root,variant=apt: test --keyring overwrites" +cat << END > shared/test.sh +#!/bin/sh +set -eu +export LC_ALL=C.UTF-8 +mkdir -p emptydir +touch emptyfile +# this overwrites the apt keyring options and should fail +ret=0 +$CMD --mode=root --variant=apt --keyring=./emptydir --keyring=./emptyfile $DEFAULT_DIST /tmp/debian-chroot $mirror || ret=\$? +rm -r /tmp/debian-chroot +rmdir emptydir +rm emptyfile +if [ "\$ret" = 0 ]; then + echo expected failure but got exit \$ret + exit 1 +fi +END if [ "$HAVE_QEMU" = "yes" ]; then ./run_qemu.sh else ./run_null.sh SUDO fi +print_header "mode=root,variant=apt: test signed-by without host keys" +cat << END > shared/test.sh +#!/bin/sh +set -eu +export LC_ALL=C.UTF-8 +echo "deb $mirror $DEFAULT_DIST main" > /etc/apt/sources.list +apt-get -o Acquire::Languages=none update +apt-get install --yes --no-install-recommends gpg +rm /etc/apt/trusted.gpg.d/*.gpg +$CMD --mode=root --variant=apt $DEFAULT_DIST /tmp/debian-chroot $mirror +printf 'deb [signed-by="/usr/share/keyrings/debian-archive-keyring.gpg"] $mirror $DEFAULT_DIST main\n' | cmp /tmp/debian-chroot/etc/apt/sources.list - +tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt - +rm -r /tmp/debian-chroot +END +if [ "$HAVE_QEMU" = "yes" ]; then + ./run_qemu.sh +else + echo "HAVE_QEMU != yes -- Skipping test..." +fi + +print_header "mode=root,variant=apt: test signed-by with host keys" +cat << END > shared/test.sh +#!/bin/sh +set -eu +export LC_ALL=C.UTF-8 +echo "deb $mirror $DEFAULT_DIST main" > /etc/apt/sources.list +apt-get -o Acquire::Languages=none update +apt-get install --yes --no-install-recommends gpg +$CMD --mode=root --variant=apt $DEFAULT_DIST /tmp/debian-chroot $mirror +printf 'deb $mirror $DEFAULT_DIST main\n' | cmp /tmp/debian-chroot/etc/apt/sources.list - +tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt - +rm -r /tmp/debian-chroot +END +if [ "$HAVE_QEMU" = "yes" ]; then + ./run_qemu.sh +else + echo "HAVE_QEMU != yes -- Skipping test..." +fi + print_header "mode=root,variant=apt: test --dpkgopt" cat << END > shared/test.sh #!/bin/sh diff --git a/make_mirror.sh b/make_mirror.sh index b747ee6..2f80651 100755 --- a/make_mirror.sh +++ b/make_mirror.sh @@ -181,7 +181,7 @@ END --or --field=Priority important --or --field=Priority standard \ --or --field=Package build-essential \) ) - pkgs="$(echo $pkgs) build-essential busybox" + pkgs="$(echo $pkgs) build-essential busybox gpg" APT_CONFIG="$rootdir/etc/apt/apt.conf" apt-get --yes install $pkgs diff --git a/mmdebstrap b/mmdebstrap index aa67861..f5f8a95 100755 --- a/mmdebstrap +++ b/mmdebstrap @@ -1009,8 +1009,8 @@ sub setup { # not needed anymore for apt 1.3 and newer print $conf "Dir::State::Status \"$options->{root}/var/lib/dpkg/status\";\n"; # for authentication, use the keyrings from the host - print $conf "Dir::Etc::Trusted \"/etc/apt/trusted.gpg\";\n"; - print $conf "Dir::Etc::TrustedParts \"/etc/apt/trusted.gpg.d\";\n"; + print $conf "Dir::Etc::Trusted \"$options->{apttrusted}\";\n"; + print $conf "Dir::Etc::TrustedParts \"$options->{apttrustedparts}\";\n"; if ($options->{variant} ne 'apt') { # apt considers itself essential. Thus, when generating an EDSP # document for an external solver, it will add the Essential:yes field @@ -1869,6 +1869,8 @@ sub main() { mode => 'auto', dpkgopts => [], aptopts => [], + apttrusted => "/etc/apt/trusted.gpg", + apttrustedparts => "/etc/apt/trusted.gpg.d", noop => [], setup_hook => [], essential_hook => [], @@ -1890,15 +1892,21 @@ sub main() { 'keyring=s' => sub { my ($opt_name, $opt_value) = @_; if ($opt_value =~ /"/) { - error "apt cannot handle paths with double quotes"; + error "--keyring: apt cannot handle paths with double quotes: $opt_value"; } if (! -e $opt_value) { error "keyring \"$opt_value\" does not exist"; } + my $abs_path = abs_path($opt_value); + if (!defined $abs_path) { + error "unable to get absolute path of --keyring: $opt_value"; + } + # since abs_path resolved all symlinks for us, we can now test + # what the actual target actually is if (-d $opt_value) { - push @{$options->{aptopts}}, "Dir::Etc::TrustedParts \"$opt_value\""; + $options->{apttrustedparts} = $opt_value; } else { - push @{$options->{aptopts}}, "Dir::Etc::Trusted \"$opt_value\""; + $options->{apttrusted} = $opt_value; } }, 's|silent' => sub { $verbosity_level = 0; }, @@ -2229,6 +2237,96 @@ sub main() { } } my $compstr = join " ", @components; + # if the currently selected apt keyrings do not contain the + # necessary key material for the chosen suite, then attempt adding + # a signed-by option + my $signedby = ''; + { + # try to guess the right keyring path for the given suite + my $keyring; + if (any {$_ eq $suite} ('potato', 'woody', 'sarge', 'etch', 'lenny', 'squeeze', 'wheezy')) { + $keyring = '/usr/share/keyrings/debian-archive-removed-keys.gpg'; + } elsif (any {$_ eq $suite} ('aequorea', 'bartholomea', 'chromodoris', 'dasyatis')) { + $keyring = '/usr/share/keyrings/tanglu-archive-keyring.gpg'; + } elsif (any {$_ eq $suite} ('kali-dev', 'kali-rolling', 'kali-bleeding-edge')) { + $keyring = '/usr/share/keyrings/kali-archive-keyring.gpg'; + } elsif (any {$_ eq $suite} ('trusty', 'xenial', 'zesty', 'artful', 'bionic', 'cosmic')) { + $keyring = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'; + } elsif (any {$_ eq $suite} ('unstable', 'stable', 'oldstable', 'jessie', 'stretch', 'buster', 'bullseye', 'bookworm')) { + $keyring = '/usr/share/keyrings/debian-archive-keyring.gpg'; + } + # we can only check if we need the signed-by entry if we u + # automatically chosen keyring exists + if (defined $keyring && -e $keyring) { + # we can only check key material if gpg is installed + my $gpghome = tempdir("mmdebstrap.gpghome.XXXXXXXXXXXX", TMPDIR => 1, CLEANUP => 1); + my @gpgcmd = ('gpg', '--quiet', '--ignore-time-conflict', '--no-options', '--no-default-keyring', '--homedir', $gpghome, '--no-auto-check-trustdb', '--trust-model', 'always'); + my ($ret, $fh, $message); + { + # change warning handler to prevent message + # Can't exec "gpg": No such file or directory + local $SIG{__WARN__} = sub { $message = shift; }; + $ret = open $fh, '-|', @gpgcmd, '--version'; + } + close $fh; # we only want to check if the gpg command exists + if ($? == 0 && defined $ret && !defined $message) { + # find all the fingerprints of the keys apt currently + # knows about + my @aptfingerprints = (); + my $collect_fingerprints = sub { + my $filename = shift; + open my $fh, '-|', @gpgcmd, '--keyring', $filename, '--with-colons', '--list-keys' // error "failed to fork(): $!"; + while (my $line = <$fh>) { + if ($line !~ /^fpr:::::::::([^:]+):/) { + next; + } + push @aptfingerprints, $1; + } + close $fh; + }; + opendir my $dh, "$options->{apttrustedparts}" or error "cannot read $options->{apttrustedparts}"; + while (my $filename = readdir $dh) { + if ($filename !~ /\.(asc|gpg)$/) { + next; + } + $collect_fingerprints->("$options->{apttrustedparts}/$filename"); + } + if (-e $options->{apttrusted}) { + $collect_fingerprints->($options->{apttrusted}); + } + # check if all fingerprints from the keyring that we + # guessed are known by apt and only add signed-by + # option if that's not the case + my @suitefingerprints = (); + open my $suitefh, '-|', @gpgcmd, '--keyring', $keyring, '--with-colons', '--list-keys' // error "failed to fork(): $!"; + while (my $line = <$suitefh>) { + if ($line !~ /^fpr:::::::::([^:]+):/) { + next; + } + # if this fingerprint is not known by apt, then we + # need to add the signed-by option + if (none { $_ eq $1 } @aptfingerprints) { + $signedby = " [signed-by=\"$keyring\"]"; + last; + } + } + close $suitefh; + if ($? != 0) { + error "gpg failed"; + } + } else { + info "gpg --version failed: cannot determine the right signed-by value" + } + remove_tree($gpghome, {error => \my $err}); + if (@$err) { + for my $diag (@$err) { + my ($file, $message) = %$diag; + if ($file eq '') { warning "general error: $message"; } + else { warning "problem unlinking $file: $message"; } + } + } + } + } if (scalar @ARGV > 0) { for my $arg (@ARGV) { if ($arg eq '-') { @@ -2237,7 +2335,7 @@ sub main() { } elsif ($arg =~ /^deb(-src)? /) { $sourceslist .= "$arg\n"; } elsif ($arg =~ /:\/\//) { - $sourceslist .= "deb $arg $suite $compstr\n"; + $sourceslist .= "deb$signedby $arg $suite $compstr\n"; } elsif (-f $arg) { open my $fh, '<', $arg or error "cannot open $arg: $!"; while (my $line = <$fh>) { @@ -2269,20 +2367,20 @@ sub main() { } elsif (any {$_ eq $suite} @kali) { $mirror = 'https://http.kali.org/kali' } - $sourceslist .= "deb $mirror $suite $compstr\n"; + $sourceslist .= "deb$signedby $mirror $suite $compstr\n"; if (any {$_ eq $suite} @ubuntustable) { - $sourceslist .= "deb $mirror $suite-updates $compstr\n"; - $sourceslist .= "deb $secmirror $suite-security $compstr\n"; + $sourceslist .= "deb$signedby $mirror $suite-updates $compstr\n"; + $sourceslist .= "deb$signedby $secmirror $suite-security $compstr\n"; } elsif (any {$_ eq $suite} @tanglustable) { - $sourceslist .= "deb $secmirror $suite-updates $compstr\n"; + $sourceslist .= "deb$signedby $secmirror $suite-updates $compstr\n"; } elsif (any {$_ eq $suite} @debstable) { - $sourceslist .= "deb $mirror $suite-updates $compstr\n"; + $sourceslist .= "deb$signedby $mirror $suite-updates $compstr\n"; if (any {$_ eq $suite} ('oldoldstable', 'oldstable', 'stable', 'jessie', 'stretch', 'buster')) { - $sourceslist .= "deb $secmirror $suite/updates $compstr\n"; + $sourceslist .= "deb$signedby $secmirror $suite/updates $compstr\n"; } else { # starting from bullseye use # https://lists.debian.org/87r26wqr2a.fsf@43-1.org - $sourceslist .= "deb $secmirror $suite-security $compstr\n"; + $sourceslist .= "deb$signedby $secmirror $suite-security $compstr\n"; } } } @@ -2841,16 +2939,31 @@ Example: Minimizing the number of packages installed from experimental =item B<--keyring>=I|I -A shorthand for using C<--aptopt='Dir::Etc::Trusted "file"'> or -C<-aptopt='Dir::Etc::TrustedParts "directory"'> when passing a file or -directory to the B<--keyring> option, respectively. B will add the -right keyring for the given I if it knows about the distribution and if -the keyring is installed in a path known by B, usually -F. If B does not know or cannot find the -right keyring for the given I it will only know about the keys that apt -on the host system knows about. If you want to prevent B from -choosing the right keyring for you for known values of I, choose an -arbitrary value for I and specify the right apt line manually. +Change the default keyring to use by apt. By default, F +and F are used. Depending on whether a file or +directory is passed to this option, the former and latter default can be +changed, respectively. Since apt only supports a single keyring file and +directory, respectively, you can B use this option to pass multiple files +and/or directories. Using the C<--keyring> argument in the following way is +equal to keeping the default: + + --keyring=/etc/apt/trusted.gpg --keyring=/etc/apt/trusted.gpg.d + +If you need to pass multiple keyrings, use the C option when +specifying the mirror like this: + + mmdebstrap mysuite out.tar "deb [signed-by=/path/to/key.gpg] http://..." + +The C option will automatically be added to the final +C if the keyring required for the selected I is not yet +trusted by apt. Automatically adding the C option in these cases +requires C to be installed. If C and C are +installed, then you can create a Ubuntu Bionic chroot on Debian like this: + + mmdebstrap bionic ubuntu-bionic.tar + +The resulting chroot will have a C with a C option +pointing to F. =item B<--dpkgopt>=I