-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add gpg_fingerprint lookup and filter (#639)
* Add gpg_fingerprint lookup. * Work around problems on some CI targets. * Use get_bin_path to find the gpg executable. Document that we need it. * Improve and test error handling. * Refactor (potentially) common code to module_utils and plugin_utils. This will be useful to create a filter version of this, and further lookups, filters, and modules. * Do not create a keyring when there isn't one. * Fixups. * Fix description. * More fixes for lookup. * Also add a gpg_fingerprint filter. * Improve formulation. Co-authored-by: Sandra McCann <[email protected]> --------- Co-authored-by: Sandra McCann <[email protected]>
- Loading branch information
1 parent
5e630ff
commit ba456c5
Showing
20 changed files
with
545 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (c) 2023, Felix Fontein <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
DOCUMENTATION = """ | ||
name: gpg_fingerprint | ||
short_description: Retrieve a GPG fingerprint from a GPG public or private key | ||
author: Felix Fontein (@felixfontein) | ||
version_added: 2.15.0 | ||
description: | ||
- "Takes the content of a private or public GPG key as input and returns its fingerprint." | ||
options: | ||
_input: | ||
description: | ||
- The content of a GPG public or private key. | ||
type: string | ||
required: true | ||
requirements: | ||
- GnuPG (C(gpg) executable) | ||
seealso: | ||
- plugin: community.crypto.gpg_fingerprint | ||
plugin_type: lookup | ||
""" | ||
|
||
EXAMPLES = """ | ||
- name: Show fingerprint of GPG public key | ||
ansible.builtin.debug: | ||
msg: "{{ lookup('file', '/path/to/public_key.gpg') | community.crypto.gpg_fingerprint }}" | ||
""" | ||
|
||
RETURN = """ | ||
_value: | ||
description: | ||
- The fingerprint of the provided public or private GPG key. | ||
type: string | ||
""" | ||
|
||
from ansible.errors import AnsibleFilterError | ||
from ansible.module_utils.common.text.converters import to_bytes, to_native | ||
from ansible.module_utils.six import string_types | ||
|
||
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, get_fingerprint_from_bytes | ||
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import PluginGPGRunner | ||
|
||
|
||
def gpg_fingerprint(input): | ||
if not isinstance(input, string_types): | ||
raise AnsibleFilterError( | ||
'The input for the community.crypto.gpg_fingerprint filter must be a string; got {type} instead'.format(type=type(input)) | ||
) | ||
try: | ||
gpg = PluginGPGRunner() | ||
return get_fingerprint_from_bytes(gpg, to_bytes(input)) | ||
except GPGError as exc: | ||
raise AnsibleFilterError(to_native(exc)) | ||
|
||
|
||
class FilterModule(object): | ||
'''Ansible jinja2 filters''' | ||
|
||
def filters(self): | ||
return { | ||
'gpg_fingerprint': gpg_fingerprint, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (c) 2023, Felix Fontein <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
DOCUMENTATION = """ | ||
name: gpg_fingerprint | ||
short_description: Retrieve a GPG fingerprint from a GPG public or private key file | ||
author: Felix Fontein (@felixfontein) | ||
version_added: 2.15.0 | ||
description: | ||
- "Takes a list of filenames pointing to GPG public or private key files. Returns the fingerprints for each of these keys." | ||
options: | ||
_terms: | ||
description: | ||
- A path to a GPG public or private key. | ||
type: list | ||
elements: path | ||
required: true | ||
requirements: | ||
- GnuPG (C(gpg) executable) | ||
seealso: | ||
- plugin: community.crypto.gpg_fingerprint | ||
plugin_type: filter | ||
""" | ||
|
||
EXAMPLES = """ | ||
- name: Show fingerprint of GPG public key | ||
ansible.builtin.debug: | ||
msg: "{{ lookup('community.crypto.gpg_fingerprint', '/path/to/public_key.gpg') }}" | ||
""" | ||
|
||
RETURN = """ | ||
_value: | ||
description: | ||
- The fingerprints of the provided public or private GPG keys. | ||
- The list has one entry for every path provided. | ||
type: list | ||
elements: string | ||
""" | ||
|
||
from ansible.plugins.lookup import LookupBase | ||
from ansible.errors import AnsibleLookupError | ||
from ansible.module_utils.common.text.converters import to_native | ||
|
||
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, get_fingerprint_from_file | ||
from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import PluginGPGRunner | ||
|
||
|
||
class LookupModule(LookupBase): | ||
def run(self, terms, variables=None, **kwargs): | ||
self.set_options(direct=kwargs) | ||
|
||
try: | ||
gpg = PluginGPGRunner(cwd=self._loader.get_basedir()) | ||
result = [] | ||
for path in terms: | ||
result.append(get_fingerprint_from_file(gpg, path)) | ||
return result | ||
except GPGError as exc: | ||
raise AnsibleLookupError(to_native(exc)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (c) 2023, Felix Fontein <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
import abc | ||
import os | ||
|
||
from ansible.module_utils import six | ||
|
||
|
||
class GPGError(Exception): | ||
pass | ||
|
||
|
||
@six.add_metaclass(abc.ABCMeta) | ||
class GPGRunner(object): | ||
@abc.abstractmethod | ||
def run_command(self, command, check_rc=True, data=None): | ||
""" | ||
Run ``[gpg] + command`` and return ``(rc, stdout, stderr)``. | ||
If ``data`` is not ``None``, it will be provided as stdin. | ||
The code assumes it is a bytes string. | ||
Returned stdout and stderr are native Python strings. | ||
Pass ``check_rc=False`` to allow return codes != 0. | ||
Raises a ``GPGError`` in case of errors. | ||
""" | ||
pass | ||
|
||
|
||
def get_fingerprint_from_stdout(stdout): | ||
lines = stdout.splitlines(False) | ||
for line in lines: | ||
if line.startswith('fpr:'): | ||
parts = line.split(':') | ||
if len(parts) <= 9 or not parts[9]: | ||
raise GPGError('Result line "{line}" does not have fingerprint as 10th component'.format(line=line)) | ||
return parts[9] | ||
raise GPGError('Cannot extract fingerprint from stdout "{stdout}"'.format(stdout=stdout)) | ||
|
||
|
||
def get_fingerprint_from_file(gpg_runner, path): | ||
if not os.path.exists(path): | ||
raise GPGError('{path} does not exist'.format(path=path)) | ||
stdout = gpg_runner.run_command( | ||
['--no-keyring', '--with-colons', '--import-options', 'show-only', '--import', path], | ||
check_rc=True, | ||
)[1] | ||
return get_fingerprint_from_stdout(stdout) | ||
|
||
|
||
def get_fingerprint_from_bytes(gpg_runner, content): | ||
stdout = gpg_runner.run_command( | ||
['--no-keyring', '--with-colons', '--import-options', 'show-only', '--import', '/dev/stdin'], | ||
data=content, | ||
check_rc=True, | ||
)[1] | ||
return get_fingerprint_from_stdout(stdout) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright (c) 2023, Felix Fontein <[email protected]> | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
from subprocess import Popen, PIPE | ||
|
||
from ansible.module_utils.common.process import get_bin_path | ||
from ansible.module_utils.common.text.converters import to_native | ||
|
||
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import GPGError, GPGRunner | ||
|
||
|
||
class PluginGPGRunner(GPGRunner): | ||
def __init__(self, executable=None, cwd=None): | ||
if executable is None: | ||
try: | ||
executable = get_bin_path('gpg') | ||
except ValueError as e: | ||
raise GPGError('Cannot find the `gpg` executable on the controller') | ||
self.executable = executable | ||
self.cwd = cwd | ||
|
||
def run_command(self, command, check_rc=True, data=None): | ||
""" | ||
Run ``[gpg] + command`` and return ``(rc, stdout, stderr)``. | ||
If ``data`` is not ``None``, it will be provided as stdin. | ||
The code assumes it is a bytes string. | ||
Returned stdout and stderr are native Python strings. | ||
Pass ``check_rc=False`` to allow return codes != 0. | ||
Raises a ``GPGError`` in case of errors. | ||
""" | ||
command = [self.executable] + command | ||
p = Popen(command, shell=False, cwd=self.cwd, stdin=PIPE, stdout=PIPE, stderr=PIPE) | ||
stdout, stderr = p.communicate(input=data) | ||
stdout = to_native(stdout, errors='surrogate_or_replace') | ||
stderr = to_native(stderr, errors='surrogate_or_replace') | ||
if check_rc and p.returncode != 0: | ||
raise GPGError('Running {cmd} yielded return code {rc} with stdout: "{stdout}" and stderr: "{stderr}")'.format( | ||
cmd=' '.join(command), | ||
rc=p.returncode, | ||
stdout=to_native(stdout, errors='surrogate_or_replace'), | ||
stderr=to_native(stderr, errors='surrogate_or_replace'), | ||
)) | ||
return p.returncode, stdout, stderr |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
azp/posix/2 | ||
destructive |
9 changes: 9 additions & 0 deletions
9
tests/integration/targets/filter_gpg_fingerprint/meta/main.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
dependencies: | ||
- prepare_jinja2_compat | ||
- setup_remote_tmp_dir | ||
- setup_gnupg |
80 changes: 80 additions & 0 deletions
80
tests/integration/targets/filter_gpg_fingerprint/tasks/main.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
- name: Run tests if GPG is available | ||
when: has_gnupg | ||
block: | ||
- name: Create GPG key | ||
ansible.builtin.command: | ||
cmd: gpg --homedir "{{ remote_tmp_dir }}" --batch --generate-key | ||
stdin: | | ||
%echo Generating a basic OpenPGP key | ||
%no-ask-passphrase | ||
%no-protection | ||
Key-Type: RSA | ||
Key-Length: 4096 | ||
Name-Real: Foo Bar | ||
Name-Email: [email protected] | ||
Expire-Date: 0 | ||
%commit | ||
%echo done | ||
register: result | ||
|
||
- name: Extract fingerprint | ||
ansible.builtin.shell: gpg --homedir "{{ remote_tmp_dir }}" --with-colons --fingerprint [email protected] | grep '^fpr:' | ||
register: fingerprints | ||
|
||
- name: Show fingerprints | ||
ansible.builtin.debug: | ||
msg: "{{ fingerprints.stdout_lines | map('split', ':') | list }}" | ||
|
||
- name: Export public key | ||
ansible.builtin.command: gpg --homedir "{{ remote_tmp_dir }}" --export --armor [email protected] | ||
register: public_key | ||
|
||
- name: Export private key | ||
ansible.builtin.command: gpg --homedir "{{ remote_tmp_dir }}" --export-secret-key --armor [email protected] | ||
register: private_key | ||
|
||
- name: Gather fingerprints | ||
ansible.builtin.set_fact: | ||
public_key_fingerprint: "{{ public_key.stdout | community.crypto.gpg_fingerprint }}" | ||
private_key_fingerprint: "{{ private_key.stdout | community.crypto.gpg_fingerprint }}" | ||
|
||
- name: Check whether fingerprints match | ||
ansible.builtin.assert: | ||
that: | ||
- public_key_fingerprint == (fingerprints.stdout_lines[0] | split(':'))[9] | ||
- private_key_fingerprint == (fingerprints.stdout_lines[0] | split(':'))[9] | ||
|
||
- name: Error scenario - wrong input type | ||
ansible.builtin.set_fact: | ||
failing_result: "{{ 42 | community.crypto.gpg_fingerprint }}" | ||
register: result | ||
ignore_errors: true | ||
|
||
- name: Check result | ||
ansible.builtin.assert: | ||
that: | ||
- result is failed | ||
- >- | ||
'The input for the community.crypto.gpg_fingerprint filter must be a string; got ' in result.msg | ||
- >- | ||
'int' in result.msg | ||
- name: Error scenario - garbage input | ||
ansible.builtin.set_fact: | ||
failing_result: "{{ 'garbage' | community.crypto.gpg_fingerprint }}" | ||
register: result | ||
ignore_errors: true | ||
|
||
- name: Check result | ||
ansible.builtin.assert: | ||
that: | ||
- result is failed | ||
- >- | ||
'Running ' in result.msg | ||
- >- | ||
('/gpg --no-keyring --with-colons --import-options show-only --import /dev/stdin yielded return code ') in result.msg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
azp/posix/2 | ||
destructive |
9 changes: 9 additions & 0 deletions
9
tests/integration/targets/lookup_gpg_fingerprint/meta/main.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
dependencies: | ||
- prepare_jinja2_compat | ||
- setup_remote_tmp_dir | ||
- setup_gnupg |
Oops, something went wrong.