Skip to content

Commit

Permalink
Support multiple keyrings in sources.list Signed-By
Browse files Browse the repository at this point in the history
A user can specify multiple fingerprints for a while now, so its seems
counter-intuitive to support only one keyring, especially if this isn't
really checked or enforced and while unlikely mixtures of both should
work properly, too, instead of a kinda random behaviour.
  • Loading branch information
DonKult committed Sep 11, 2018
1 parent ff8fa4a commit 8375d5b
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 106 deletions.
11 changes: 7 additions & 4 deletions apt-pkg/contrib/gpgv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,20 @@ void ExecGPGV(std::string const &File, std::string const &FileGPG,
Args.push_back(aptkey.c_str());
Args.push_back("--quiet");
Args.push_back("--readonly");
if (key.empty() == false)
auto const keysFileFpr = VectorizeString(key, ',');
for (auto const &k: keysFileFpr)
{
if (key[0] == '/')
if (unlikely(k.empty()))
continue;
if (k[0] == '/')
{
Args.push_back("--keyring");
Args.push_back(key.c_str());
Args.push_back(k.c_str());
}
else
{
Args.push_back("--keyid");
Args.push_back(key.c_str());
Args.push_back(k.c_str());
}
}
Args.push_back("verify");
Expand Down
157 changes: 86 additions & 71 deletions cmdline/apt-key.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,74 @@ eval "$(apt-config shell ARCHIVE_KEYRING_URI APT::Key::ArchiveKeyringURI)"

aptkey_echo() { echo "$@"; }

find_gpgv_status_fd() {
while [ -n "$1" ]; do
if [ "$1" = '--status-fd' ]; then
shift
echo "$1"
break
fi
shift
done
}
GPGSTATUSFD="$(find_gpgv_status_fd "$@")"

apt_warn() {
if [ -z "$GPGHOMEDIR" ]; then
echo >&2 'W:' "$@"
else
echo 'W:' "$@" > "${GPGHOMEDIR}/aptwarnings.log"
fi
if [ -n "$GPGSTATUSFD" ]; then
echo >&${GPGSTATUSFD} '[APTKEY:] WARNING' "$@"
fi
}
apt_error() {
if [ -z "$GPGHOMEDIR" ]; then
echo >&2 'E:' "$@"
else
echo 'E:' "$@" > "${GPGHOMEDIR}/aptwarnings.log"
fi
if [ -n "$GPGSTATUSFD" ]; then
echo >&${GPGSTATUSFD} '[APTKEY:] ERROR' "$@"
fi
}

cleanup_gpg_home() {
if [ -z "$GPGHOMEDIR" ]; then return; fi
if [ -s "$GPGHOMEDIR/aptwarnings.log" ]; then
cat >&2 "$GPGHOMEDIR/aptwarnings.log"
fi
if command_available 'gpgconf'; then
GNUPGHOME="${GPGHOMEDIR}" gpgconf --kill all >/dev/null 2>&1 || true
fi
rm -rf "$GPGHOMEDIR"
}

# gpg needs (in different versions more or less) files to function correctly,
# so we give it its own homedir and generate some valid content for it later on
create_gpg_home() {
# for cases in which we want to cache a homedir due to expensive setup
if [ -n "$GPGHOMEDIR" ]; then
return
fi
if [ -n "$TMPDIR" ]; then
# tmpdir is a directory and current user has rwx access to it
# same tests as in apt-pkg/contrib/fileutl.cc GetTempDir()
if [ ! -d "$TMPDIR" ] || [ ! -r "$TMPDIR" ] || [ ! -w "$TMPDIR" ] || [ ! -x "$TMPDIR" ]; then
unset TMPDIR
fi
fi
GPGHOMEDIR="$(mktemp --directory --tmpdir 'apt-key-gpghome.XXXXXXXXXX')"
CURRENTTRAP="${CURRENTTRAP} cleanup_gpg_home;"
trap "${CURRENTTRAP}" 0 HUP INT QUIT ILL ABRT FPE SEGV PIPE TERM
if [ -z "$GPGHOMEDIR" ]; then
apt_error "Could not create temporary gpg home directory in $TMPDIR (wrong permissions?)"
exit 28
fi
chmod 700 "$GPGHOMEDIR"
}

requires_root() {
if [ "$(id -u)" -ne 0 ]; then
apt_error "This command can only be used by root."
Expand Down Expand Up @@ -282,7 +350,7 @@ foreach_keyring_do() {
shift
# if a --keyring was given, just work on this one
if [ -n "$FORCED_KEYRING" ]; then
$ACTION "$TRUSTEDFILE" "$@"
$ACTION "$FORCED_KEYRING" "$@"
else
# otherwise all known keyrings are up for inspection
if accessible_file_exists "$TRUSTEDFILE" && is_supported_keyring "$TRUSTEDFILE"; then
Expand Down Expand Up @@ -525,11 +593,26 @@ while [ -n "$1" ]; do
case "$1" in
--keyring)
shift
TRUSTEDFILE="$1"
FORCED_KEYRING="$1"
if [ -z "$FORCED_KEYRING" -o "$FORCED_KEYRING" = '/dev/null' ]; then
TRUSTEDFILE="$1"
FORCED_KEYRING="$1"
elif [ "$TRUSTEDFILE" = "$FORCED_KEYRING" ]; then
create_gpg_home
FORCED_KEYRING="${GPGHOMEDIR}/mergedkeyrings.gpg"
echo -n '' > "$FORCED_KEYRING"
chmod 0644 -- "$FORCED_KEYRING"
catfile "$TRUSTEDFILE" "$FORCED_KEYRING"
catfile "$1" "$FORCED_KEYRING"
else
catfile "$1" "$FORCED_KEYRING"
fi
;;
--keyid)
shift
if [ -n "$FORCED_KEYID" ]; then
apt_error 'Specifying --keyid multiple times is not supported'
exit 1
fi
FORCED_KEYID="$1"
;;
--secret-keyring)
Expand Down Expand Up @@ -582,74 +665,6 @@ if [ -z "$command" ]; then
fi
shift

find_gpgv_status_fd() {
while [ -n "$1" ]; do
if [ "$1" = '--status-fd' ]; then
shift
echo "$1"
break
fi
shift
done
}
GPGSTATUSFD="$(find_gpgv_status_fd "$@")"

apt_warn() {
if [ -z "$GPGHOMEDIR" ]; then
echo >&2 'W:' "$@"
else
echo 'W:' "$@" > "${GPGHOMEDIR}/aptwarnings.log"
fi
if [ -n "$GPGSTATUSFD" ]; then
echo >&${GPGSTATUSFD} '[APTKEY:] WARNING' "$@"
fi
}
apt_error() {
if [ -z "$GPGHOMEDIR" ]; then
echo >&2 'E:' "$@"
else
echo 'E:' "$@" > "${GPGHOMEDIR}/aptwarnings.log"
fi
if [ -n "$GPGSTATUSFD" ]; then
echo >&${GPGSTATUSFD} '[APTKEY:] ERROR' "$@"
fi
}

cleanup_gpg_home() {
if [ -z "$GPGHOMEDIR" ]; then return; fi
if [ -s "$GPGHOMEDIR/aptwarnings.log" ]; then
cat >&2 "$GPGHOMEDIR/aptwarnings.log"
fi
if command_available 'gpgconf'; then
GNUPGHOME="${GPGHOMEDIR}" gpgconf --kill all >/dev/null 2>&1 || true
fi
rm -rf "$GPGHOMEDIR"
}

# gpg needs (in different versions more or less) files to function correctly,
# so we give it its own homedir and generate some valid content for it later on
create_gpg_home() {
# for cases in which we want to cache a homedir due to expensive setup
if [ -n "$GPGHOMEDIR" ]; then
return
fi
if [ -n "$TMPDIR" ]; then
# tmpdir is a directory and current user has rwx access to it
# same tests as in apt-pkg/contrib/fileutl.cc GetTempDir()
if [ ! -d "$TMPDIR" ] || [ ! -r "$TMPDIR" ] || [ ! -w "$TMPDIR" ] || [ ! -x "$TMPDIR" ]; then
unset TMPDIR
fi
fi
GPGHOMEDIR="$(mktemp --directory --tmpdir 'apt-key-gpghome.XXXXXXXXXX')"
CURRENTTRAP="${CURRENTTRAP} cleanup_gpg_home;"
trap "${CURRENTTRAP}" 0 HUP INT QUIT ILL ABRT FPE SEGV PIPE TERM
if [ -z "$GPGHOMEDIR" ]; then
apt_error "Could not create temporary gpg home directory in $TMPDIR (wrong permissions?)"
exit 28
fi
chmod 700 "$GPGHOMEDIR"
}

prepare_gpg_home() {
# crude detection if we are called from a maintainerscript where the
# package depends on gnupg or not. We accept recommends here as
Expand Down
29 changes: 17 additions & 12 deletions doc/sources.list.5.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
&apt-email;
&apt-product;
<!-- The last update date -->
<date>2018-02-27T00:00:00Z</date>
<date>2018-08-17T00:00:00Z</date>
</refentryinfo>

<refmeta>
Expand Down Expand Up @@ -294,17 +294,22 @@ deb-src [ option1=value1 option2=value2 ] uri suite [component1] [component2] [.
</para></listitem>

<listitem><para><option>Signed-By</option> (<option>signed-by</option>)
is either an absolute path to a keyring file (has to be
accessible and readable for the <literal>_apt</literal> user,
so ensure everyone has read-permissions on the file) or one or
more fingerprints of keys either in the
<filename>trusted.gpg</filename> keyring or in the
keyrings in the <filename>trusted.gpg.d/</filename> directory
(see <command>apt-key fingerprint</command>). If the option is
set, only the key(s) in this keyring or only the keys with these
fingerprints are used for the &apt-secure; verification of this
repository. Defaults to the value of the option with the same name
if set in the previously acquired <filename>Release</filename> file.
is an option to require a repository to pass &apt-secure; verification
with a certain set of keys rather than all trusted keys apt has configured.
It is specified as a list of absolute paths to keyring files (have to be
accessible and readable for the <literal>_apt</literal> system user,
so ensure everyone has read-permissions on the file) and fingerprints
of keys to select from these keyrings. If no keyring files are specified
the default is the <filename>trusted.gpg</filename> keyring and
all keyrings in the <filename>trusted.gpg.d/</filename> directory
(see <command>apt-key fingerprint</command>). If no fingerprint is
specified all keys in the keyrings are selected. A fingerprint will
accept also all signatures by a subkey of this key, if this isn't
desired an exclamation mark (<literal>!</literal>) can be appended to
the fingerprint to disable this behaviour.
The option defaults to the value of the option with the same name
if set in the previously acquired <filename>Release</filename> file
of this repository (only fingerprints can be specified there through).
Otherwise all keys in the trusted keyrings are considered valid
signers for this repository.
</para></listitem>
Expand Down
40 changes: 29 additions & 11 deletions methods/gpgv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ class GPGVMethod : public aptMethod
{
private:
string VerifyGetSigners(const char *file, const char *outfile,
std::string const &key,
vector<string> const &keyFpts,
vector<string> const &keyFiles,
vector<string> &GoodSigners,
vector<string> &BadSigners,
vector<string> &WorthlessSigners,
Expand Down Expand Up @@ -146,7 +147,8 @@ static void PushEntryWithUID(std::vector<std::string> &Signers, char * const buf
Signers.push_back(msg);
}
string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile,
std::string const &key,
vector<string> const &keyFpts,
vector<string> const &keyFiles,
vector<string> &GoodSigners,
vector<string> &BadSigners,
vector<string> &WorthlessSigners,
Expand All @@ -159,7 +161,6 @@ string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile,
std::clog << "inside VerifyGetSigners" << std::endl;

int fd[2];
bool const keyIsID = (key.empty() == false && key[0] != '/');

if (pipe(fd) < 0)
return "Couldn't create pipe";
Expand All @@ -168,7 +169,15 @@ string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile,
if (pid < 0)
return string("Couldn't spawn new process") + strerror(errno);
else if (pid == 0)
ExecGPGV(outfile, file, 3, fd, (keyIsID ? "" : key));
{
std::ostringstream keys;
if (keyFiles.empty() == false)
{
std::copy(keyFiles.begin(), keyFiles.end()-1, std::ostream_iterator<std::string>(keys, ","));
keys << *keyFiles.rbegin();
}
ExecGPGV(outfile, file, 3, fd, keys.str());
}
close(fd[1]);

FILE *pipein = fdopen(fd[0], "r");
Expand Down Expand Up @@ -259,18 +268,21 @@ string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile,

// apt-key has a --keyid parameter, but this requires gpg, so we call it without it
// and instead check after the fact which keyids where used for verification
if (keyIsID == true)
if (keyFpts.empty() == false)
{
if (Debug == true)
std::clog << "GoodSigs needs to be limited to keyid(s) " << key << std::endl;
auto const limitedTo = VectorizeString(key, ',');
{
std::clog << "GoodSigs needs to be limited to keyid(s): ";
std::copy(keyFpts.begin(), keyFpts.end(), std::ostream_iterator<std::string>(std::clog, ", "));
std::clog << "\n";
}
std::vector<std::string> filteredGood;
for (auto &&good: GoodSigners)
{
if (Debug == true)
std::clog << "Key " << good << " is good sig, is it also a valid and allowed one? ";
bool found = false;
for (auto l : limitedTo)
for (auto l : keyFpts)
{
bool exactKey = false;
if (APT::String::Endswith(l, "!"))
Expand Down Expand Up @@ -353,7 +365,7 @@ string GPGVMethod::VerifyGetSigners(const char *file, const char *outfile,
}
else if (WEXITSTATUS(status) == 0)
{
if (keyIsID)
if (keyFpts.empty() == false)
{
// gpgv will report success, but we want to enforce a certain keyring
// so if we haven't found the key the valid we found is in fact invalid
Expand All @@ -379,7 +391,6 @@ bool GPGVMethod::URIAcquire(std::string const &Message, FetchItem *Itm)
{
URI const Get = Itm->Uri;
string const Path = Get.Host + Get.Path; // To account for relative paths
std::string const key = LookupTag(Message, "Signed-By");
vector<string> GoodSigners;
vector<string> BadSigners;
// a worthless signature is a expired or revoked one
Expand All @@ -391,8 +402,15 @@ bool GPGVMethod::URIAcquire(std::string const &Message, FetchItem *Itm)
Res.Filename = Itm->DestFile;
URIStart(Res);

std::vector<std::string> keyFpts, keyFiles;
for (auto &&key : VectorizeString(LookupTag(Message, "Signed-By"), ','))
if (key.empty() == false && key[0] == '/')
keyFiles.emplace_back(std::move(key));
else
keyFpts.emplace_back(std::move(key));

// Run apt-key on file, extract contents and get the key ID of the signer
string const msg = VerifyGetSigners(Path.c_str(), Itm->DestFile.c_str(), key,
string const msg = VerifyGetSigners(Path.c_str(), Itm->DestFile.c_str(), keyFpts, keyFiles,
GoodSigners, BadSigners, WorthlessSigners,
SoonWorthlessSigners, NoPubKeySigners);
if (_error->PendingError())
Expand Down
16 changes: 16 additions & 0 deletions test/integration/test-apt-key
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ gpg: unchanged: 1' aptkey --fakeroot update
testsuccess test -s "${TMPWORKINGDIRECTORY}/aptkey.export"
testsuccess test -s "${TMPWORKINGDIRECTORY}/aptkey.exportall"

msgtest 'Check that multiple keys can be' 'exported'
aptkey export 'Sixpack' 'Expired' > "${TMPWORKINGDIRECTORY}/aptkey.export" 2>/dev/null
aptkey --keyring "${KEYDIR}/rexexpired.pub.${EXT}" \
--keyring "${ROOTDIR}/etc/apt/trusted.gpg.d/joesixpack.${EXT}" exportall > "${TMPWORKINGDIRECTORY}/aptkey.exportall"
testsuccess --nomsg cmp "${TMPWORKINGDIRECTORY}/aptkey.export" "${TMPWORKINGDIRECTORY}/aptkey.exportall"
testsuccess test -s "${TMPWORKINGDIRECTORY}/aptkey.export"
testsuccess test -s "${TMPWORKINGDIRECTORY}/aptkey.exportall"

msgtest 'Execute update again to trigger removal of' 'Rex Expired key'
${TESTSTATE} --nomsg aptkey --fakeroot update

Expand Down Expand Up @@ -274,6 +282,14 @@ gpg: unchanged: 1' aptkey --fakeroot update
msgtest 'Test verify a file' 'with good keyring'
testsuccess --nomsg aptkey --quiet --readonly --keyring "${KEYDIR}/testcase-multikey.pub.${EXT}" verify "${SIGNATURE}.gpg" "${SIGNATURE}"

msgtest 'Test verify a file' 'with good keyrings 1'
testsuccess --nomsg aptkey --quiet --readonly --keyring "${KEYDIR}/joesixpack.pub.${EXT}" \
--keyring "${KEYDIR}/marvinparanoid.pub.${EXT}" verify "${SIGNATURE}.gpg" "${SIGNATURE}"

msgtest 'Test verify a file' 'with good keyrings 2'
testsuccess --nomsg aptkey --quiet --readonly --keyring "${KEYDIR}/marvinparanoid.pub.${EXT}" \
--keyring "${KEYDIR}/joesixpack.pub.${EXT}" verify "${SIGNATURE}.gpg" "${SIGNATURE}"

msgtest 'Test fail verify a file' 'with bad keyring'
testfailure --nomsg aptkey --quiet --readonly --keyring "${KEYDIR}/joesixpack.pub.${EXT}" verify "${SIGNATURE}.gpg" "${SIGNATURE}"

Expand Down
Loading

0 comments on commit 8375d5b

Please sign in to comment.