78
78
default: /etc/pacman.d/gnupg
79
79
state:
80
80
description:
81
- - Ensures that the key is present (added) or absent (revoked).
81
+ - Ensures that the key is V( present) (added) or V( absent) (revoked).
82
82
default: present
83
83
choices: [absent, present]
84
84
type: str
85
+ ensure_trusted:
86
+ description:
87
+ - Ensure that the key is trusted (signed by the Pacman machine key and not expired).
88
+ - This option has been added in community.general 10.6.0.
89
+ type: bool
90
+ default: false
85
91
"""
86
92
87
93
EXAMPLES = r"""
129
135
from ansible .module_utils .common .text .converters import to_native
130
136
131
137
138
+ class GpgListResult (object ):
139
+ """Wraps gpg --list-* output."""
140
+
141
+ def __init__ (self , line ):
142
+ self ._parts = line .split (':' )
143
+
144
+ @property
145
+ def kind (self ):
146
+ return self ._parts [0 ]
147
+
148
+ @property
149
+ def valid (self ):
150
+ return self ._parts [1 ]
151
+
152
+ @property
153
+ def is_fully_valid (self ):
154
+ return self .valid == 'f'
155
+
156
+ @property
157
+ def key (self ):
158
+ return self ._parts [4 ]
159
+
160
+ @property
161
+ def user_id (self ):
162
+ return self ._parts [9 ]
163
+
164
+
165
+ def gpg_get_first (lines , kind , attr ):
166
+ for line in lines :
167
+ glr = GpgListResult (line )
168
+ if glr .kind == kind :
169
+ return getattr (glr , attr )
170
+
171
+
172
+ def gpg_gather_all (lines , kind , attr ):
173
+ result = []
174
+ for line in lines :
175
+ glr = GpgListResult (line )
176
+ if glr .kind == kind :
177
+ result .append (getattr (glr , attr ))
178
+ return result
179
+
180
+
132
181
class PacmanKey (object ):
133
182
def __init__ (self , module ):
134
183
self .module = module
135
184
# obtain binary paths for gpg & pacman-key
136
- self .gpg = module .get_bin_path ('gpg' , required = True )
137
- self .pacman_key = module .get_bin_path ('pacman-key' , required = True )
185
+ self .gpg_binary = module .get_bin_path ('gpg' , required = True )
186
+ self .pacman_key_binary = module .get_bin_path ('pacman-key' , required = True )
138
187
139
188
# obtain module parameters
140
189
keyid = module .params ['id' ]
@@ -146,47 +195,71 @@ def __init__(self, module):
146
195
force_update = module .params ['force_update' ]
147
196
keyring = module .params ['keyring' ]
148
197
state = module .params ['state' ]
198
+ ensure_trusted = module .params ['ensure_trusted' ]
149
199
self .keylength = 40
150
200
151
201
# sanitise key ID & check if key exists in the keyring
152
202
keyid = self .sanitise_keyid (keyid )
153
- key_present = self .key_in_keyring (keyring , keyid )
203
+ key_validity = self .key_validity (keyring , keyid )
204
+ key_present = len (key_validity ) > 0
205
+ key_valid = any (key_validity )
154
206
155
207
# check mode
156
208
if module .check_mode :
157
- if state == " present" :
209
+ if state == ' present' :
158
210
changed = (key_present and force_update ) or not key_present
211
+ if not changed and ensure_trusted :
212
+ changed = not (key_valid and self .key_is_trusted (keyring , keyid ))
159
213
module .exit_json (changed = changed )
160
- elif state == "absent" :
161
- if key_present :
162
- module .exit_json (changed = True )
163
- module .exit_json (changed = False )
214
+ if state == 'absent' :
215
+ module .exit_json (changed = key_present )
164
216
165
- if state == "present" :
166
- if key_present and not force_update :
217
+ if state == 'present' :
218
+ trusted = key_valid and self .key_is_trusted (keyring , keyid )
219
+ if not force_update and key_present and (not ensure_trusted or trusted ):
167
220
module .exit_json (changed = False )
168
-
221
+ changed = False
169
222
if data :
170
223
file = self .save_key (data )
171
224
self .add_key (keyring , file , keyid , verify )
172
- module . exit_json ( changed = True )
225
+ changed = True
173
226
elif file :
174
227
self .add_key (keyring , file , keyid , verify )
175
- module . exit_json ( changed = True )
228
+ changed = True
176
229
elif url :
177
230
data = self .fetch_key (url )
178
231
file = self .save_key (data )
179
232
self .add_key (keyring , file , keyid , verify )
180
- module . exit_json ( changed = True )
233
+ changed = True
181
234
elif keyserver :
182
235
self .recv_key (keyring , keyid , keyserver )
183
- module .exit_json (changed = True )
184
- elif state == "absent" :
236
+ changed = True
237
+ if changed or (ensure_trusted and not trusted ):
238
+ self .lsign_key (keyring = keyring , keyid = keyid )
239
+ changed = True
240
+ module .exit_json (changed = changed )
241
+ elif state == 'absent' :
185
242
if key_present :
186
243
self .remove_key (keyring , keyid )
187
244
module .exit_json (changed = True )
188
245
module .exit_json (changed = False )
189
246
247
+ def gpg (self , args , keyring = None , ** kwargs ):
248
+ cmd = [self .gpg_binary ]
249
+ if keyring :
250
+ cmd .append ('--homedir={keyring}' .format (keyring = keyring ))
251
+ cmd .extend (['--no-permission-warning' , '--with-colons' , '--quiet' , '--batch' , '--no-tty' ])
252
+ return self .module .run_command (cmd + args , ** kwargs )
253
+
254
+ def pacman_key (self , args , keyring , ** kwargs ):
255
+ return self .module .run_command (
256
+ [self .pacman_key_binary , '--gpgdir' , keyring ] + args ,
257
+ ** kwargs )
258
+
259
+ def pacman_machine_key (self , keyring ):
260
+ unused_rc , stdout , unused_stderr = self .gpg (['--list-secret-key' ], keyring = keyring )
261
+ return gpg_get_first (stdout .splitlines (), 'sec' , 'key' )
262
+
190
263
def is_hexadecimal (self , string ):
191
264
"""Check if a given string is valid hexadecimal"""
192
265
try :
@@ -216,14 +289,11 @@ def fetch_key(self, url):
216
289
217
290
def recv_key (self , keyring , keyid , keyserver ):
218
291
"""Receives key via keyserver"""
219
- cmd = [self .pacman_key , '--gpgdir' , keyring , '--keyserver' , keyserver , '--recv-keys' , keyid ]
220
- self .module .run_command (cmd , check_rc = True )
221
- self .lsign_key (keyring , keyid )
292
+ self .pacman_key (['--keyserver' , keyserver , '--recv-keys' , keyid ], keyring = keyring , check_rc = True )
222
293
223
294
def lsign_key (self , keyring , keyid ):
224
295
"""Locally sign key"""
225
- cmd = [self .pacman_key , '--gpgdir' , keyring ]
226
- self .module .run_command (cmd + ['--lsign-key' , keyid ], check_rc = True )
296
+ self .pacman_key (['--lsign-key' , keyid ], keyring = keyring , check_rc = True )
227
297
228
298
def save_key (self , data ):
229
299
"Saves key data to a temporary file"
@@ -238,14 +308,11 @@ def add_key(self, keyring, keyfile, keyid, verify):
238
308
"""Add key to pacman's keyring"""
239
309
if verify :
240
310
self .verify_keyfile (keyfile , keyid )
241
- cmd = [self .pacman_key , '--gpgdir' , keyring , '--add' , keyfile ]
242
- self .module .run_command (cmd , check_rc = True )
243
- self .lsign_key (keyring , keyid )
311
+ self .pacman_key (['--add' , keyfile ], keyring = keyring , check_rc = True )
244
312
245
313
def remove_key (self , keyring , keyid ):
246
314
"""Remove key from pacman's keyring"""
247
- cmd = [self .pacman_key , '--gpgdir' , keyring , '--delete' , keyid ]
248
- self .module .run_command (cmd , check_rc = True )
315
+ self .pacman_key (['--delete' , keyid ], keyring = keyring , check_rc = True )
249
316
250
317
def verify_keyfile (self , keyfile , keyid ):
251
318
"""Verify that keyfile matches the specified key ID"""
@@ -254,48 +321,29 @@ def verify_keyfile(self, keyfile, keyid):
254
321
elif keyid is None :
255
322
self .module .fail_json (msg = "expected a key ID, got none" )
256
323
257
- rc , stdout , stderr = self .module .run_command (
258
- [
259
- self .gpg ,
260
- '--with-colons' ,
261
- '--with-fingerprint' ,
262
- '--batch' ,
263
- '--no-tty' ,
264
- '--show-keys' ,
265
- keyfile
266
- ],
324
+ rc , stdout , stderr = self .gpg (
325
+ ['--with-fingerprint' , '--show-keys' , keyfile ],
267
326
check_rc = True ,
268
327
)
269
328
270
- extracted_keyid = None
271
- for line in stdout .splitlines ():
272
- if line .startswith ('fpr:' ):
273
- extracted_keyid = line .split (':' )[9 ]
274
- break
275
-
329
+ extracted_keyid = gpg_get_first (stdout .splitlines (), 'fpr' , 'user_id' )
276
330
if extracted_keyid != keyid :
277
331
self .module .fail_json (msg = "key ID does not match. expected %s, got %s" % (keyid , extracted_keyid ))
278
332
279
- def key_in_keyring (self , keyring , keyid ):
280
- "Check if the key ID is in pacman's keyring"
281
- rc , stdout , stderr = self .module .run_command (
282
- [
283
- self .gpg ,
284
- '--with-colons' ,
285
- '--batch' ,
286
- '--no-tty' ,
287
- '--no-default-keyring' ,
288
- '--keyring=%s/pubring.gpg' % keyring ,
289
- '--list-keys' , keyid
290
- ],
291
- check_rc = False ,
292
- )
333
+ def key_validity (self , keyring , keyid ):
334
+ "Check if the key ID is in pacman's keyring and not expired"
335
+ rc , stdout , stderr = self .gpg (['--no-default-keyring' , '--list-keys' , keyid ], keyring = keyring , check_rc = False )
293
336
if rc != 0 :
294
337
if stderr .find ("No public key" ) >= 0 :
295
- return False
338
+ return []
296
339
else :
297
340
self .module .fail_json (msg = "gpg returned an error: %s" % stderr )
298
- return True
341
+ return gpg_gather_all (stdout .splitlines (), 'uid' , 'is_fully_valid' )
342
+
343
+ def key_is_trusted (self , keyring , keyid ):
344
+ """Check if key is signed and not expired."""
345
+ unused_rc , stdout , unused_stderr = self .gpg (['--check-signatures' , keyid ], keyring = keyring )
346
+ return self .pacman_machine_key (keyring ) in gpg_gather_all (stdout .splitlines (), 'sig' , 'key' )
299
347
300
348
301
349
def main ():
@@ -309,6 +357,7 @@ def main():
309
357
verify = dict (type = 'bool' , default = True ),
310
358
force_update = dict (type = 'bool' , default = False ),
311
359
keyring = dict (type = 'path' , default = '/etc/pacman.d/gnupg' ),
360
+ ensure_trusted = dict (type = 'bool' , default = False ),
312
361
state = dict (type = 'str' , default = 'present' , choices = ['absent' , 'present' ]),
313
362
),
314
363
supports_check_mode = True ,
0 commit comments