44
55from __future__ import annotations
66
7+ import re
78from datetime import timedelta
89from urllib .parse import urlparse
910
@@ -45,57 +46,139 @@ def _simulate_then_perform(command: str):
4546 yield noninteractive_apt (command )
4647
4748
48- @operation ()
49- def key (src : str | None = None , keyserver : str | None = None , keyid : str | list [str ] | None = None ):
49+ def _sanitize_apt_keyring_name (name : str ) -> str :
5050 """
51- Add apt gpg keys with ``apt-key``.
52-
53- + src: filename or URL
54- + keyserver: URL of keyserver to fetch key from
55- + keyid: key ID or list of key IDs when using keyserver
51+ Produce a filesystem-friendly name from an URL host/basename or a local filename.
52+ """
53+ name = name .strip ().lower ()
54+ name = re .sub (r"[^\w.-]+" , "_" , name )
55+ name = re .sub (r"_+" , "_" , name ).strip ("_." )
56+ return name or "apt-keyring"
5657
57- keyserver/id:
58- These must be provided together.
5958
60- .. warning::
61- ``apt-key`` is deprecated in Debian, it is recommended NOT to use this
62- operation and instead follow the instructions here:
59+ def _derive_dest_from_src_and_keyids (src : str | None , keyids : list [str ] | None , dest : str | None ) -> str :
60+ """
61+ Compute a stable destination path in /etc/apt/keyrings/.
62+ Priority:
63+ 1) explicit dest if provided
64+ 2) from src (URL host + basename, or local basename)
65+ 3) from keyids (joined)
66+ 4) fallback "apt-keyring.gpg"
67+ """
68+ if dest :
69+ # Ensure it ends with .gpg and is absolute under /etc/apt/keyrings
70+ if not dest .endswith (".gpg" ):
71+ dest += ".gpg"
72+ if not dest .startswith ("/" ):
73+ dest = f"/etc/apt/keyrings/{ dest } "
74+ return dest
75+
76+ base = None
77+ if src :
78+ parsed = urlparse (src )
79+ if parsed .scheme and parsed .netloc :
80+ host = _sanitize_apt_keyring_name (parsed .netloc .replace (":" , "_" ))
81+ bn = _sanitize_apt_keyring_name ((parsed .path .rsplit ("/" , 1 )[- 1 ] or "key" ).replace (".asc" , "" ).replace (".gpg" , "" ))
82+ base = f"{ host } -{ bn } "
83+ else :
84+ bn = _sanitize_apt_keyring_name (src .rsplit ("/" , 1 )[- 1 ].replace (".asc" , "" ).replace (".gpg" , "" ))
85+ base = bn or "key"
86+ elif keyids :
87+ base = "keyserver-" + _sanitize_apt_keyring_name ("-" .join (keyids ))
88+ else :
89+ base = "apt-keyring"
6390
64- https://wiki.debian.org/DebianRepository/UseThirdParty
91+ return f"/etc/apt/keyrings/ { base } .gpg"
6592
66- **Examples:**
6793
68- .. code:: python
69-
70- # Note: If using URL, wget is assumed to be installed.
94+ @operation ()
95+ def key (
96+ src : str | None = None ,
97+ keyserver : str | None = None ,
98+ keyid : str | list [str ] | None = None ,
99+ dest : str | None = None ,
100+ ):
101+ """
102+ Add apt GPG keys *without* apt-key:
103+ - Keys are stored under /etc/apt/keyrings/<name>.gpg (binary, dearmored if needed).
104+ - You must reference the resulting file in your apt source via `signed-by=...`.
105+
106+ Args:
107+ src: filename or URL to a key (ASCII .asc or binary .gpg)
108+ keyserver: keyserver URL for fetching keys by ID
109+ keyid: key ID or list of key IDs (required with keyserver)
110+ dest: optional keyring filename/path ('.gpg' will be enforced, defaults under /etc/apt/keyrings)
111+
112+ Behavior:
113+ - Idempotent via AptKeys: if the key IDs are already present in any apt keyring, nothing is changed.
114+ - If src is ASCII (.asc), it will be dearmored; if binary (.gpg), it's copied as-is.
115+ - Keyserver flow uses a temporary GNUPGHOME, then exports and dearmors to the destination keyring.
116+
117+ Examples:
71118 apt.key(
72- name="Add the Docker apt gpg key",
73- src="https://download.docker.com/linux/ubuntu/gpg",
119+ name="Add Docker apt GPG key",
120+ src="https://download.docker.com/linux/debian/gpg",
121+ dest="docker.gpg",
74122 )
75123
76124 apt.key(
77125 name="Install VirtualBox key",
78126 src="https://www.virtualbox.org/download/oracle_vbox_2016.asc",
127+ dest="oracle-virtualbox.gpg",
128+ )
129+
130+ apt.key(
131+ name="Fetch keys from keyserver",
132+ keyserver="hkps://keyserver.ubuntu.com",
133+ keyid=["0xD88E42B4", "0x7EA0A9C3"],
134+ dest="vendor-archive.gpg",
79135 )
80136 """
81137
138+ # Gather currently installed keys (across trusted.gpg.d/, keyrings/, etc.)
82139 existing_keys = host .get_fact (AptKeys )
83140
141+ # --- src branch: install a key from URL or local file ---
84142 if src :
85- key_data = host .get_fact (GpgKey , src = src )
86- if key_data :
87- keyid = list (key_data .keys ())
143+ key_data = host .get_fact (GpgKey , src = src ) # Parses the key(s) from src to extract key IDs
144+ keyids_from_src = list (key_data .keys ()) if key_data else []
145+
146+ # If we don't know the IDs (eg. unreachable URL), we cannot determine idempotency -> try to install.
147+ # Otherwise, skip if all key IDs are already present.
148+ if (not keyids_from_src ) or (not all (kid in existing_keys for kid in keyids_from_src )):
149+ dest_path = _derive_dest_from_src_and_keyids (src , keyids_from_src or None , dest )
88150
89- if not keyid or not all (kid in existing_keys for kid in keyid ):
90- # If URL, wget the key to stdout and pipe into apt-key, because the "adv"
91- # apt-key passes to gpg which doesn't always support https!
92151 if urlparse (src ).scheme :
93- yield "(wget -O - {0} || curl -sSLf {0}) | apt-key add -" .format (src )
152+ # Remote source: download to a temp file, then install/dearmor accordingly
153+ yield (
154+ "sh -c 'set -e;"
155+ " install -d -m 0755 /etc/apt/keyrings;"
156+ " tmp=$(mktemp);"
157+ f" (wget -qO \" $tmp\" { src } || curl -sSLf -o \" $tmp\" { src } );"
158+ " if grep -q \" BEGIN PGP PUBLIC KEY BLOCK\" \" $tmp\" ; then"
159+ f" gpg --batch --dearmor -o \" { dest_path } \" \" $tmp\" ;"
160+ " else"
161+ f" install -m 0644 \" $tmp\" \" { dest_path } \" ;"
162+ " fi;"
163+ " rm -f \" $tmp\" ;"
164+ f" chmod 0644 \" { dest_path } \" '"
165+ )
94166 else :
95- yield "apt-key add {0}" .format (src )
167+ # Local file already present on the target
168+ yield (
169+ "sh -c 'set -e;"
170+ " install -d -m 0755 /etc/apt/keyrings;"
171+ f" if grep -q \" BEGIN PGP PUBLIC KEY BLOCK\" \" { src } \" ; then"
172+ f" gpg --batch --dearmor -o \" { dest_path } \" \" { src } \" ;"
173+ " else"
174+ f" install -m 0644 \" { src } \" \" { dest_path } \" ;"
175+ " fi;"
176+ f" chmod 0644 \" { dest_path } \" '"
177+ )
96178 else :
97- host .noop ("All keys from {0 } are already available in the apt keychain" . format ( src ) )
179+ host .noop (f "All keys from { src } are already available in the apt keychain" )
98180
181+ # --- keyserver branch: fetch one or multiple keys by ID ---
99182 if keyserver :
100183 if not keyid :
101184 raise OperationError ("`keyid` must be provided with `keyserver`" )
@@ -105,16 +188,22 @@ def key(src: str | None = None, keyserver: str | None = None, keyid: str | list[
105188
106189 needed_keys = sorted (set (keyid ) - set (existing_keys .keys ()))
107190 if needed_keys :
108- yield "apt-key adv --keyserver {0} --recv-keys {1}" .format (
109- keyserver ,
110- " " .join (needed_keys ),
191+ dest_path = _derive_dest_from_src_and_keyids (None , needed_keys , dest )
192+ joined = " " .join (needed_keys )
193+ # Use a temporary GNUPGHOME so we don't pollute the system/user keyring,
194+ # then export and dearmor to the APT keyring destination.
195+ yield (
196+ "sh -c 'set -e;"
197+ " install -d -m 0755 /etc/apt/keyrings;"
198+ " tmp=$(mktemp -d);"
199+ " export GNUPGHOME=\" $tmp\" ;"
200+ f" gpg --batch --keyserver \" { keyserver } \" --recv-keys { joined } ;"
201+ f" gpg --batch --export { joined } | gpg --batch --dearmor -o \" { dest_path } \" ;"
202+ " rm -rf \" $tmp\" ;"
203+ f" chmod 0644 \" { dest_path } \" '"
111204 )
112205 else :
113- host .noop (
114- "Keys {0} are already available in the apt keychain" .format (
115- ", " .join (keyid ),
116- ),
117- )
206+ host .noop (f"Keys { ', ' .join (keyid )} are already available in the apt keychain" )
118207
119208
120209@operation ()
0 commit comments