4
4
5
5
from __future__ import annotations
6
6
7
+ import re
7
8
from datetime import timedelta
8
9
from urllib .parse import urlparse
9
10
@@ -45,57 +46,139 @@ def _simulate_then_perform(command: str):
45
46
yield noninteractive_apt (command )
46
47
47
48
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 :
50
50
"""
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"
56
57
57
- keyserver/id:
58
- These must be provided together.
59
58
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"
63
90
64
- https://wiki.debian.org/DebianRepository/UseThirdParty
91
+ return f"/etc/apt/keyrings/ { base } .gpg"
65
92
66
- **Examples:**
67
93
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:
71
118
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",
74
122
)
75
123
76
124
apt.key(
77
125
name="Install VirtualBox key",
78
126
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",
79
135
)
80
136
"""
81
137
138
+ # Gather currently installed keys (across trusted.gpg.d/, keyrings/, etc.)
82
139
existing_keys = host .get_fact (AptKeys )
83
140
141
+ # --- src branch: install a key from URL or local file ---
84
142
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 )
88
150
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!
92
151
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
+ )
94
166
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
+ )
96
178
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" )
98
180
181
+ # --- keyserver branch: fetch one or multiple keys by ID ---
99
182
if keyserver :
100
183
if not keyid :
101
184
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[
105
188
106
189
needed_keys = sorted (set (keyid ) - set (existing_keys .keys ()))
107
190
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 } \" '"
111
204
)
112
205
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" )
118
207
119
208
120
209
@operation ()
0 commit comments