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