77"""
88
99import logging
10+ from datetime import date
1011
1112import salt .utils .dictupdate
1213import salt .utils .immutabletypes as immutabletypes
@@ -32,6 +33,24 @@ class KeyNotContained(CommandExecutionError):
3233 """
3334
3435
36+ def _expired_subkeys (key , max_days = 100 ):
37+ """
38+ From a single key of the output of gpg.list_keys, iterate over
39+ subkeys and return those that have expired recently.
40+ We don't want to keep checking old subkeys for updates.
41+ """
42+ ret = {}
43+ if not max_days :
44+ return ret
45+ for subkey in key .get ("subkeys" , []):
46+ if subkey .get ("expired" ):
47+ # Only try to refresh subkeys that have expired recently
48+ expired_date = date .fromisoformat (subkey ["expires" ])
49+ if (date .today () - expired_date ).days <= max_days :
50+ ret [subkey ["keyid" ]] = subkey
51+ return ret
52+
53+
3554def present (
3655 name ,
3756 keys = None ,
@@ -43,6 +62,7 @@ def present(
4362 source = None ,
4463 skip_keyserver = False ,
4564 text = None ,
65+ subkey_maxage = 100 ,
4666 ** kwargs ,
4767):
4868 """
@@ -109,6 +129,17 @@ def present(
109129 Requires python-gnupg v0.5.1.
110130
111131 .. versionadded:: 3008.0
132+
133+ subkey_maxage
134+ If the managed key has expired subkeys, this state attempts an update.
135+ Since sometimes keys have long expired subkeys, it filters eligible subkeys
136+ that trigger the update check.
137+ This parameter specifies the maximum number of days since a subkey's
138+ expiration for the key to be eligible. Defaults to ``100``, meaning
139+ subkeys that have expired more than 100 days ago do not trigger an attempt.
140+ Set this to a falsy value to skip the explicit management of subkeys.
141+
142+ .. versionadded:: 3008.0
112143 """
113144
114145 ret = {"name" : name , "result" : True , "changes" : {}, "comment" : "" }
@@ -132,12 +163,17 @@ def present(
132163
133164 current_keys = {}
134165 expired_keys = []
166+ expired_subkeys = {}
135167 for key in _current_keys :
136168 keyid = key ["keyid" ]
137169 current_keys [keyid ] = {}
138170 current_keys [keyid ]["trust" ] = key ["trust" ]
171+ current_keys [keyid ]["subkeys" ] = key .get ("subkeys" , {})
139172 if key .get ("expired" ):
140173 expired_keys .append (keyid )
174+ key_expired_subs = _expired_subkeys (key , max_days = subkey_maxage )
175+ if key_expired_subs :
176+ expired_subkeys [keyid ] = key_expired_subs
141177
142178 if not keys :
143179 keys = name
@@ -149,22 +185,34 @@ def present(
149185 for key in keys :
150186 key_res [key ] = []
151187 try :
152- refresh = key in expired_keys
188+ is_expired = key in expired_keys
189+ has_expired_subkeys = key in expired_subkeys
190+ refresh = is_expired or has_expired_subkeys
153191 if key in current_keys and not refresh :
154192 key_res [key ].append (f"GPG Public Key { key } already in keychain" )
155193 continue
156194 if __opts__ ["test" ]:
157195 ret ["result" ] = None
158- key_res [key ] = [f"Would have added { key } to GPG keychain" ]
159- salt .utils .dictupdate .set_dict_key_value (
160- ret , f"changes:{ key } :added" , True
161- )
162196 if refresh :
163- key_res [key ][- 1 ] += " (the existing one was expired)"
197+ if is_expired :
198+ key_res [key ] = [
199+ f"Would have attempted to update { key } because it is expired"
200+ ]
201+ else :
202+ key_res [key ] = [
203+ f"Would have attempted to update { key } because it has expired subkeys:"
204+ ] + [
205+ f" - { subkey } (expired on { data ['expires' ]} )"
206+ for subkey , data in expired_subkeys [key ].items ()
207+ ]
164208 salt .utils .dictupdate .set_dict_key_value (
165209 ret , f"changes:{ key } :refresh" , True
166210 )
167211 else :
212+ key_res [key ] = [f"Would have added { key } to GPG keychain" ]
213+ salt .utils .dictupdate .set_dict_key_value (
214+ ret , f"changes:{ key } :added" , True
215+ )
168216 current_keys [key ] = {"trust" : "unknown" }
169217 continue
170218 result = {}
@@ -185,8 +233,11 @@ def present(
185233 result ["res" ] = any (
186234 "updated: new" in x for x in result ["message" ]
187235 )
236+ if not is_expired and not result ["res" ]:
237+ # Don't fail if we're only updating expired subkeys though
238+ result ["res" ] = None
188239 result ["message" ] = "\n " .join (result ["message" ])
189- if (not result or result ["res" ] is False ) and source :
240+ if (not result or not result ["res" ]) and source :
190241 if not isinstance (source , list ):
191242 source = [source ]
192243 prev_msg = ""
@@ -224,24 +275,62 @@ def present(
224275 result ["message" ]
225276 + f"\n The new key { key } could not be retrieved though."
226277 )
227- salt .utils .dictupdate .set_dict_key_value (ret , f"changes:{ key } :added" , True )
278+ if not refresh :
279+ salt .utils .dictupdate .set_dict_key_value (
280+ ret , f"changes:{ key } :added" , True
281+ )
228282 if new_key .get ("expired" ):
229283 raise CommandExecutionError (
230284 result ["message" ] + f"\n The new key { key } is expired though."
231285 )
232- key_res [key ].append (f"Added { key } to GPG keychain" )
233- current_keys [key ] = {"trust" : new_key ["trust" ]}
234286 if refresh :
235- key_res [key ][- 1 ] += " (the existing one was expired)"
236- salt .utils .dictupdate .set_dict_key_value (
237- ret , f"changes:{ key } :refresh" , True
238- )
287+ added_subs = {
288+ subkey ["keyid" ] for subkey in new_key .get ("subkeys" , {})
289+ } - {subkey ["keyid" ] for subkey in current_keys [key ]["subkeys" ]}
290+ updated_subs = set ()
291+ if has_expired_subkeys :
292+ after_expired_subs = _expired_subkeys (
293+ new_key , max_days = subkey_maxage
294+ )
295+ updated_subs = set (expired_subkeys [key ]) - set (after_expired_subs )
296+
297+ if is_expired :
298+ key_res [key ].append (f"Updated { key } because it was expired" )
299+ salt .utils .dictupdate .set_dict_key_value (
300+ ret , f"changes:{ key } :refresh" , True
301+ )
302+ elif added_subs or updated_subs :
303+ key_res [key ].append (
304+ f"Updated { key } because it had expired subkeys."
305+ )
306+ salt .utils .dictupdate .set_dict_key_value (
307+ ret , f"changes:{ key } :refresh" , True
308+ )
309+ if added_subs :
310+ salt .utils .dictupdate .set_dict_key_value (
311+ ret , f"changes:{ key } :subkeys:added" , list (added_subs )
312+ )
313+ if updated_subs :
314+ salt .utils .dictupdate .set_dict_key_value (
315+ ret , f"changes:{ key } :subkeys:extended" , list (updated_subs )
316+ )
317+ else :
318+ key_res [key ].append (
319+ f"Attempted to update { key } because it has expired subkeys, but no new signatures or keys were found"
320+ )
321+ else :
322+ key_res [key ].append (f"Added { key } to GPG keychain" )
323+ current_keys [key ] = {"trust" : new_key ["trust" ]}
239324 except (CommandExecutionError , SaltInvocationError ) as err :
240325 ret ["result" ] = False
241- if refresh :
326+ if is_expired :
242327 key_res [key ].append (
243328 "Existing key is expired, tried to fetch updated one"
244329 )
330+ elif has_expired_subkeys :
331+ key_res [key ].append (
332+ "Existing key has expired subkeys, tried to refresh"
333+ )
245334 key_res [key ].extend (str (err ).splitlines ())
246335
247336 # Now all possible keys are present, manage their trust if requested
0 commit comments