Skip to content

pacman_key: support checking for expired and untrusted keys #9950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

AlD
Copy link

@AlD AlD commented Mar 30, 2025

Adds state trusted.

Fixes #9949

SUMMARY

Adds functionality for pacman_key to check for key validity in terms of expiration and signature of the pacman machine key.

Introduces a new trusted state for the new functionality in order to preserve current behavior of present, although this should arguably be merged into present.

Moves gpg/pacman-key parsing into a trivial wrapper (GpgListResult) and adds two helpers that either return a single match or a collection of all matches for a certain gpg output item type.

Moves binary invocations with common flags into module methods.

Replaces --keyring with equivalent --homedir flag in order to allow for signature checks to work.

ISSUE TYPE
  • Feature Pull Request
COMPONENT NAME

pacman_key

ADDITIONAL INFORMATION

The existing test output already used an expired key, hence the GPG_SHOWKEY_OUTPUT{ → _EXPIRED} renaming of the variable holding the unchanged test data .

@ansibullbot
Copy link
Collaborator

@ansibullbot ansibullbot added feature This issue/PR relates to a feature request module module new_contributor Help guide this first time contributor plugins plugin (any type) tests tests unit tests/unit labels Mar 30, 2025
@ansibullbot

This comment was marked as outdated.

@ansibullbot ansibullbot added the needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR label Mar 30, 2025
Copy link
Collaborator

@felixfontein felixfontein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution! I've added some first comments.

Regarding the interface: I'm not sure whether a new state is the best way to do this. I think it would be better to be able to specify the update mode, because what you want to do is tell the module to update the key if the fingerprint matches, but other parameters do not.

Right now the module supports two update modes: "always" (force_update=true), and "compare fingerprint" (force_update=false). Having another mode "full idempotent" (checks ID and other parameters) is basically what you want, I think?

@AlD
Copy link
Author

AlD commented Mar 31, 2025

Thanks for the quick review, Felix.

Regarding the interface: I'm not sure whether a new state is the best way to do this. I think it would be better to be able to specify the update mode, because what you want to do is tell the module to update the key if the fingerprint matches, but other parameters do not.

I agree that a separate state is not very intuitive. IMHO this should just be the default behavior of the present state. I can't really think of a realistic scenario where a user would prefer using force_update to update an expired key.

In that vein I wouldn't even make this new functionality configurable, because even if a user prefers to keep an expired key, having an import attempt of the expired key on each run is likely not noticeable.

@ansibullbot

This comment was marked as outdated.

@ansibullbot

This comment was marked as outdated.

@ansibullbot

This comment was marked as outdated.

@ansibullbot ansibullbot removed the needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR label Mar 31, 2025
@felixfontein felixfontein added check-before-release PR will be looked at again shortly before release and merged if possible. backport-10 Automatically create a backport for the stable-10 branch labels Apr 1, 2025
@grawlinson
Copy link
Contributor

I'm happy with this PR. :)

Copy link
Collaborator

@russoz russoz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small suggestion on handling the gpg command, other than that I do not have any strong opinion about solving this with an extra state or some other way, so I'll say it LGTM

Comment on lines +241 to +252
def gpg(self, args, keyring=None, **kwargs):
cmd = [self.gpg_binary]
if keyring:
cmd.append('--homedir={keyring}'.format(keyring=keyring))
cmd.extend(['--no-permission-warning', '--with-colons', '--quiet', '--batch', '--no-tty'])
return self.module.run_command(cmd + args, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to consider using CmdRunner. See https://docs.ansible.com/ansible/latest/collections/community/general/docsite/guide_cmdrunner.html for more details.

@@ -78,9 +78,9 @@
default: /etc/pacman.d/gnupg
state:
description:
- Ensures that the key is present (added) or absent (revoked).
- Ensures that the key is present (added), trusted (signed and not expired) or absent (revoked).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the PR ensure that the key is not expired? If the key provided to the module is already expired, I don't see how the module can change that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't extend keys of course, you're right. It just won't return a success status if the key is expired.

Maybe "Ensures/checks" would be clearer? How long are these descriptions supposed to be?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The descriptions should be long enough that it's clear what the option actually does.

I would move the description for trusted into a new paragraph (instead of cramming everything into a single paragraph).

@@ -78,9 +78,9 @@
default: /etc/pacman.d/gnupg
state:
description:
- Ensures that the key is present (added) or absent (revoked).
- Ensures that the key is present (added), trusted (signed and not expired) or absent (revoked).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should mention when trusted was added:

Suggested change
- Ensures that the key is present (added), trusted (signed and not expired) or absent (revoked).
- Ensures that the key is V(present) (added), V(trusted) (signed and not expired) or V(absent) (revoked).
- The state V(trusted) has been added in community.general 10.6.0.

Also formatting the state names as values is IMO a good idea.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're not opposed to folding the new functionality into the present state, I think that would be preferable. IMHO the currently implemented functionality of the present state does not solve a real life use case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a breaking change, since this considerably changes the behavior of state=present. Since we don't accept breaking changes this isn't really acceptable.

(That's why I would solve this not with a new state, but with an option. That way we can deprecate its default, and eventually change it to the better behavior, and fix the issue that way.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Finally got around to making the change to make it an option now.

I squashed all commits now, but I could restore the individual commits as well of course. Just let me know what's easier to review. Not super familiar with GitHub review workflows.

@ansibullbot ansibullbot added stale_ci CI is older than 7 days, rerun before merging and removed stale_ci CI is older than 7 days, rerun before merging labels Apr 12, 2025
@ansibullbot ansibullbot added the stale_ci CI is older than 7 days, rerun before merging label Apr 23, 2025
@ansibullbot ansibullbot removed the stale_ci CI is older than 7 days, rerun before merging label May 5, 2025
@russoz russoz changed the title Support checking for expired and untrusted keys pacman_key: support checking for expired and untrusted keys May 10, 2025
Copy link
Collaborator

@russoz russoz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @AlD

A couple of suggestions, mostly about naming and readability, but also some simplifications.

def key_is_trusted(self, keyring, keyid):
"""Check if key is signed and not expired."""
unused_rc, stdout, unused_stderr = self.gpg(['--check-signatures', keyid], keyring=keyring)
return self.pacman_machine_key(keyring) in gpg_gather_all(stdout.splitlines(), 'sig', 'key')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function gpg_gather_all() does more than just gathering, it also filters. TBH, I think that function is doing more than it should and the code would be leaner if it only returned the list of GpgListResult objects. Then, the code here could be changed to:

Suggested change
return self.pacman_machine_key(keyring) in gpg_gather_all(stdout.splitlines(), 'sig', 'key')
return self.pacman_machine_key(keyring) in [x.key for x in gpg_gather_all(stdout.splitlines() if x.kind == 'sig')

I think that makes it easier for the reader to understand what is really going on.

Same applies to line 341 above.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that it gathers the key of all kind GPG results. I'm not really convinced that the list comprehension is easier to understand tbh.

Copy link
Collaborator

@russoz russoz May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, list comprehensions some people love them, some people don't. You can guess my type ;-).

Anyways, I'm OK without the comprehension, but simple fact that you had to add more words to explain what the function does is a good sign that its name could be improved. Maybe gather_key_from_all() ? Your thinking makes sense for you who wrote this about now. Someone else, one year from now, will read this code out of the blue and they will have to go up and down the file, read that function, see what it does, just to figure out what took a couple of words to explain. So, the point is, do the explaining now, save everybody else (including yourself 1, 3 or 5 years from now) that time later on.

Comment on lines +172 to +178
def gpg_gather_all(lines, kind, attr):
result = []
for line in lines:
glr = GpgListResult(line)
if glr.kind == kind:
result.append(getattr(glr, attr))
return result
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As suggested before, this could/should be simpler, as:

Suggested change
def gpg_gather_all(lines, kind, attr):
result = []
for line in lines:
glr = GpgListResult(line)
if glr.kind == kind:
result.append(getattr(glr, attr))
return result
def gpg_gather_all(lines):
return [GpgListResult(line) for line in lines]

Comment on lines +165 to +169
def gpg_get_first(lines, kind, attr):
for line in lines:
glr = GpgListResult(line)
if glr.kind == kind:
return getattr(glr, attr)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And that would make this one:

Suggested change
def gpg_get_first(lines, kind, attr):
for line in lines:
glr = GpgListResult(line)
if glr.kind == kind:
return getattr(glr, attr)
def gpg_get_first(lines, kind, attr):
for glr in gpg_gather_all(lines):
if glr.kind == kind:
return getattr(glr, attr)

return getattr(glr, attr)


def gpg_gather_all(lines, kind, attr):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename it to parse_gpg_list?

if key_present:
self.remove_key(keyring, keyid)
module.exit_json(changed=True)
module.exit_json(changed=False)

def gpg(self, args, keyring=None, **kwargs):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently **kwargs is here just to handle the different cases of check_rc. That being the case, I would suggest to drop the ** thing and make it explicit:

Suggested change
def gpg(self, args, keyring=None, **kwargs):
def gpg(self, args, keyring=None, check_rc=True):

or whichever default value you prefer, then adjust the calls to that method accordingly - I mean, the calls will work as they are right now, but you might want to remove the extra parameter for the default value (True in my suggestion).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't do anything with check_rc. IMHO **kwargs conveys that it's only passed through.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@russoz means that kwargs right now is only ever used for check_rc, so it's better to make this more explicit.

The same is true for pacman_key()'s **kwargs.

Copy link
Author

@AlD AlD May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I got that. What I was trying to say was that both functions are just wrappers around gpg/pacman-key via module.run_command, and having the parameters of the wrapped function not named explicitly helps conveying which function (wrapper/wrapped) owns which set of parameters. I acknowledge that the benefit may be slim as the size of each set is currently small (2 and 1).

For some empirical evidence, the top answers in https://stackoverflow.com/questions/1415812/why-use-kwargs-in-python-what-are-some-real-world-advantages-over-using-named for example mention wrappers as a valid use case for usage of **kwargs, so I think the use here matches common expectations.

In addition to the semantics it e.g. enables IDEs that support it to show all parameters + any present doc strings supported by run_command on these functions, without having to duplicate them.

That said, if you feel strongly about it, happy to change it. Just wanted to clarify that it was a deliberate choice.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that strong. I still think the readability would be improved by naming - or removing that parameter as it is not used - but happy to merge the PR as-is wrt this.

@AlD
Copy link
Author

AlD commented May 13, 2025

Thanks a lot for the suggestions. Happy to discuss everything further of course.

@ansibullbot ansibullbot added needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR and removed needs_revision This PR fails CI tests or a maintainer has requested a review/revision of the PR labels May 13, 2025
@ansibullbot ansibullbot added the stale_ci CI is older than 7 days, rerun before merging label May 23, 2025
@felixfontein felixfontein removed the backport-10 Automatically create a backport for the stable-10 branch label May 24, 2025
- Ensure that the key is trusted (signed by the Pacman machine key and not expired).
type: bool
default: false
version_added: 10.7.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That ship has sailed, please update to

Suggested change
version_added: 10.7.0
version_added: 11.0.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
check-before-release PR will be looked at again shortly before release and merged if possible. feature This issue/PR relates to a feature request module module new_contributor Help guide this first time contributor plugins plugin (any type) stale_ci CI is older than 7 days, rerun before merging tests tests unit tests/unit
Projects
None yet
Development

Successfully merging this pull request may close these issues.

pacman_key: Doesn't verify that keys are signed and not expired
5 participants