summaryrefslogtreecommitdiffstats
path: root/apt/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'apt/auth.py')
-rw-r--r--apt/auth.py311
1 files changed, 311 insertions, 0 deletions
diff --git a/apt/auth.py b/apt/auth.py
new file mode 100644
index 0000000..6d50616
--- /dev/null
+++ b/apt/auth.py
@@ -0,0 +1,311 @@
+#!/usr/bin/python3
+# auth - authentication key management
+#
+# Copyright (c) 2004 Canonical
+# Copyright (c) 2012 Sebastian Heinlein
+#
+# Author: Michael Vogt <mvo@debian.org>
+# Sebastian Heinlein <devel@glatzor.de>
+#
+# 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, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+"""Handle GnuPG keys used to trust signed repositories."""
+
+import errno
+import os
+import os.path
+import shutil
+import subprocess
+import sys
+import tempfile
+
+import apt_pkg
+from apt_pkg import gettext as _
+
+
+class AptKeyError(Exception):
+ pass
+
+
+class AptKeyIDTooShortError(AptKeyError):
+ """Internal class do not rely on it."""
+
+
+class TrustedKey:
+
+ """Represents a trusted key."""
+
+ def __init__(self, name: str, keyid: str, date: str) -> None:
+ self.raw_name = name
+ # Allow to translated some known keys
+ self.name = _(name)
+ self.keyid = keyid
+ self.date = date
+
+ def __str__(self) -> str:
+ return f"{self.name}\n{self.keyid} {self.date}"
+
+
+def _call_apt_key_script(*args: str, **kwargs: str | None) -> str:
+ """Run the apt-key script with the given arguments."""
+ conf = None
+ cmd = [apt_pkg.config.find_file("Dir::Bin::Apt-Key", "/usr/bin/apt-key")]
+ cmd.extend(args)
+ env = os.environ.copy()
+ env["LANG"] = "C"
+ env["APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE"] = "1"
+ try:
+ if apt_pkg.config.find_dir("Dir") != "/":
+ # If the key is to be installed into a chroot we have to export the
+ # configuration from the chroot to the apt-key script by using
+ # a temporary APT_CONFIG file. The apt-key script uses apt-config
+ # shell internally
+ conf = tempfile.NamedTemporaryFile(prefix="apt-key", suffix=".conf")
+ conf.write(apt_pkg.config.dump().encode("UTF-8"))
+ conf.flush()
+ env["APT_CONFIG"] = conf.name
+ proc = subprocess.Popen(
+ cmd,
+ env=env,
+ universal_newlines=True,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+
+ stdin = kwargs.get("stdin", None)
+
+ output, stderr = proc.communicate(stdin) # type: str, str
+
+ if proc.returncode:
+ raise AptKeyError(
+ "The apt-key script failed with return code %s:\n"
+ "%s\n"
+ "stdout: %s\n"
+ "stderr: %s" % (proc.returncode, " ".join(cmd), output, stderr)
+ )
+ elif stderr:
+ sys.stderr.write(stderr) # Forward stderr
+
+ return output.strip()
+ finally:
+ if conf is not None:
+ conf.close()
+
+
+def add_key_from_file(filename: str) -> None:
+ """Import a GnuPG key file to trust repositores signed by it.
+
+ Keyword arguments:
+ filename -- the absolute path to the public GnuPG key file
+ """
+ if not os.path.abspath(filename):
+ raise AptKeyError("An absolute path is required: %s" % filename)
+ if not os.access(filename, os.R_OK):
+ raise AptKeyError("Key file cannot be accessed: %s" % filename)
+ _call_apt_key_script("add", filename)
+
+
+def add_key_from_keyserver(keyid: str, keyserver: str) -> None:
+ """Import a GnuPG key file to trust repositores signed by it.
+
+ Keyword arguments:
+ keyid -- the long keyid (fingerprint) of the key, e.g.
+ A1BD8E9D78F7FE5C3E65D8AF8B48AD6246925553
+ keyserver -- the URL or hostname of the key server
+ """
+ tmp_keyring_dir = tempfile.mkdtemp()
+ try:
+ _add_key_from_keyserver(keyid, keyserver, tmp_keyring_dir)
+ except Exception:
+ raise
+ finally:
+ # We are racing with gpg when removing sockets, so ignore
+ # failure to delete non-existing files.
+ def onerror(
+ func: object, path: str, exc_info: tuple[type, Exception, object]
+ ) -> None:
+ if isinstance(exc_info[1], OSError) and exc_info[1].errno == errno.ENOENT:
+ return
+ raise
+
+ shutil.rmtree(tmp_keyring_dir, onerror=onerror)
+
+
+def _add_key_from_keyserver(keyid: str, keyserver: str, tmp_keyring_dir: str) -> None:
+ if len(keyid.replace(" ", "").replace("0x", "")) < (160 / 4):
+ raise AptKeyIDTooShortError("Only fingerprints (v4, 160bit) are supported")
+ # create a temp keyring dir
+ tmp_secret_keyring = os.path.join(tmp_keyring_dir, "secring.gpg")
+ tmp_keyring = os.path.join(tmp_keyring_dir, "pubring.gpg")
+ # default options for gpg
+ gpg_default_options = [
+ "gpg",
+ "--no-default-keyring",
+ "--no-options",
+ "--homedir",
+ tmp_keyring_dir,
+ ]
+ # download the key to a temp keyring first
+ res = subprocess.call(
+ gpg_default_options
+ + [
+ "--secret-keyring",
+ tmp_secret_keyring,
+ "--keyring",
+ tmp_keyring,
+ "--keyserver",
+ keyserver,
+ "--recv",
+ keyid,
+ ]
+ )
+ if res != 0:
+ raise AptKeyError(f"recv from '{keyserver}' failed for '{keyid}'")
+ # FIXME:
+ # - with gnupg 1.4.18 the downloaded key is actually checked(!),
+ # i.e. gnupg will not import anything that the server sends
+ # into the keyring, so the below checks are now redundant *if*
+ # gnupg 1.4.18 is used
+
+ # now export again using the long key id (to ensure that there is
+ # really only this one key in our keyring) and not someone MITM us
+ tmp_export_keyring = os.path.join(tmp_keyring_dir, "export-keyring.gpg")
+ res = subprocess.call(
+ gpg_default_options
+ + [
+ "--keyring",
+ tmp_keyring,
+ "--output",
+ tmp_export_keyring,
+ "--export",
+ keyid,
+ ]
+ )
+ if res != 0:
+ raise AptKeyError("export of '%s' failed", keyid)
+ # now verify the fingerprint, this is probably redundant as we
+ # exported by the fingerprint in the previous command but its
+ # still good paranoia
+ output = subprocess.Popen(
+ gpg_default_options
+ + [
+ "--keyring",
+ tmp_export_keyring,
+ "--fingerprint",
+ "--batch",
+ "--fixed-list-mode",
+ "--with-colons",
+ ],
+ stdout=subprocess.PIPE,
+ universal_newlines=True,
+ ).communicate()[0]
+ got_fingerprint = None
+ for line in output.splitlines():
+ if line.startswith("fpr:"):
+ got_fingerprint = line.split(":")[9]
+ # stop after the first to ensure no subkey trickery
+ break
+ # strip the leading "0x" is there is one and uppercase (as this is
+ # what gnupg is using)
+ signing_key_fingerprint = keyid.replace("0x", "").upper()
+ if got_fingerprint != signing_key_fingerprint:
+ # make the error match what gnupg >= 1.4.18 will output when
+ # it checks the key itself before importing it
+ raise AptKeyError(
+ f"recv from '{keyserver}' failed for '{signing_key_fingerprint}'"
+ )
+ # finally add it
+ add_key_from_file(tmp_export_keyring)
+
+
+def add_key(content: str) -> None:
+ """Import a GnuPG key to trust repositores signed by it.
+
+ Keyword arguments:
+ content -- the content of the GnuPG public key
+ """
+ _call_apt_key_script("adv", "--quiet", "--batch", "--import", "-", stdin=content)
+
+
+def remove_key(fingerprint: str) -> None:
+ """Remove a GnuPG key to no longer trust repositores signed by it.
+
+ Keyword arguments:
+ fingerprint -- the fingerprint identifying the key
+ """
+ _call_apt_key_script("rm", fingerprint)
+
+
+def export_key(fingerprint: str) -> str:
+ """Return the GnuPG key in text format.
+
+ Keyword arguments:
+ fingerprint -- the fingerprint identifying the key
+ """
+ return _call_apt_key_script("export", fingerprint)
+
+
+def update() -> str:
+ """Update the local keyring with the archive keyring and remove from
+ the local keyring the archive keys which are no longer valid. The
+ archive keyring is shipped in the archive-keyring package of your
+ distribution, e.g. the debian-archive-keyring package in Debian.
+ """
+ return _call_apt_key_script("update")
+
+
+def net_update() -> str:
+ """Work similar to the update command above, but get the archive
+ keyring from an URI instead and validate it against a master key.
+ This requires an installed wget(1) and an APT build configured to
+ have a server to fetch from and a master keyring to validate. APT
+ in Debian does not support this command and relies on update
+ instead, but Ubuntu's APT does.
+ """
+ return _call_apt_key_script("net-update")
+
+
+def list_keys() -> list[TrustedKey]:
+ """Returns a list of TrustedKey instances for each key which is
+ used to trust repositories.
+ """
+ # The output of `apt-key list` is difficult to parse since the
+ # --with-colons parameter isn't user
+ output = _call_apt_key_script(
+ "adv", "--with-colons", "--batch", "--fixed-list-mode", "--list-keys"
+ )
+ res = []
+ for line in output.split("\n"):
+ fields = line.split(":")
+ if fields[0] == "pub":
+ keyid = fields[4]
+ if fields[0] == "uid":
+ uid = fields[9]
+ creation_date = fields[5]
+ key = TrustedKey(uid, keyid, creation_date)
+ res.append(key)
+ return res
+
+
+if __name__ == "__main__":
+ # Add some known keys we would like to see translated so that they get
+ # picked up by gettext
+ lambda: _("Ubuntu Archive Automatic Signing Key <ftpmaster@ubuntu.com>")
+ lambda: _("Ubuntu CD Image Automatic Signing Key <cdimage@ubuntu.com>")
+
+ apt_pkg.init()
+ for trusted_key in list_keys():
+ print(trusted_key)