mirror of
https://github.com/ansible/ansible.git
synced 2026-02-03 20:40:24 -05:00
Merge 8a73319028 into 7f17759bfe
This commit is contained in:
commit
a9b58e20f8
3 changed files with 305 additions and 16 deletions
|
|
@ -0,0 +1,2 @@
|
|||
minor_changes:
|
||||
- deb822_repository - The signed_by parameter now supports multiple keys. Input changed from string to list of strings. (https://github.com/ansible/ansible/issues/85700)
|
||||
|
|
@ -113,11 +113,14 @@ options:
|
|||
type: bool
|
||||
signed_by:
|
||||
description:
|
||||
- Either a URL to a GPG key, absolute path to a keyring file, one or
|
||||
more fingerprints of keys either in the C(trusted.gpg) keyring or in
|
||||
the keyrings in the C(trusted.gpg.d/) directory, or an ASCII armored
|
||||
GPG public key block.
|
||||
type: str
|
||||
- A string or list of strings that can be one of the below.
|
||||
- a URL to a GPG key
|
||||
- the absolute path to a keyring file
|
||||
- one or more fingerprints of keys either in the C(trusted.gpg) keyring or in
|
||||
the keyrings in the C(trusted.gpg.d/) directory
|
||||
- An ASCII armored GPG public key block.
|
||||
type: list
|
||||
elements: str
|
||||
suites:
|
||||
description:
|
||||
- >-
|
||||
|
|
@ -216,6 +219,18 @@ EXAMPLES = """
|
|||
components: stable
|
||||
architectures: amd64
|
||||
signed_by: https://download.example.com/linux/ubuntu/gpg
|
||||
|
||||
- name: Add repo using multiple keys
|
||||
deb822_repository:
|
||||
name: example
|
||||
types: deb
|
||||
uris: https://download.example.com/linux/ubuntu
|
||||
suites: '{{ ansible_distribution_release }}'
|
||||
components: stable
|
||||
architectures: amd64
|
||||
signed_by:
|
||||
- https://download.example.com/linux/ubuntu/gpg
|
||||
- https://download.example-two.com/linux/ubuntu/gpg
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
|
@ -342,10 +357,164 @@ def is_armored(b_data):
|
|||
return b'-----BEGIN PGP PUBLIC KEY BLOCK-----' in b_data
|
||||
|
||||
|
||||
PGP_BLOCK_ONLY_RE = re.compile(
|
||||
r"""
|
||||
\A
|
||||
-----BEGIN\ PGP\ PUBLIC\ KEY\ BLOCK-----\n
|
||||
(?:[A-Za-z0-9+/= ]*\n)* # base64 and armor headers
|
||||
-----END\ PGP\ PUBLIC\ KEY\ BLOCK-----\n?
|
||||
\Z
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def is_only_one_armored_block(value: str) -> bool:
|
||||
return bool(PGP_BLOCK_ONLY_RE.match(value))
|
||||
|
||||
|
||||
def contains_exactly_one_public_key(module, value: str) -> bool:
|
||||
rc, stdout, stderr = module.run_command(
|
||||
["gpg", "--batch", "--with-colons", "--show-keys"],
|
||||
data=value,
|
||||
)
|
||||
|
||||
if rc != 0:
|
||||
return False
|
||||
|
||||
pubs = [l for l in stdout.splitlines() if l.startswith("pub:")]
|
||||
return len(pubs) == 1
|
||||
|
||||
|
||||
def is_strict_single_inline_pgp_key(module, value: str) -> bool:
|
||||
value = value.strip("\n")
|
||||
return (
|
||||
is_only_one_armored_block(value)
|
||||
and contains_exactly_one_public_key(module, value)
|
||||
)
|
||||
|
||||
|
||||
# GPG fingerprints are 40 hex characters, optionally with spaces
|
||||
FINGERPRINT_RE = re.compile(r'^[A-Fa-f0-9]{4}(\s?[A-Fa-f0-9]{4}){9}$')
|
||||
|
||||
|
||||
def is_fingerprint(value: str) -> bool:
|
||||
"""
|
||||
Check if a value is a GPG key fingerprint.
|
||||
|
||||
Fingerprints are 40 hex characters, optionally separated by spaces.
|
||||
Examples:
|
||||
- "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
|
||||
- "A1B2 C3D4 E5F6 A1B2 C3D4 E5F6 A1B2 C3D4 E5F6 A1B2"
|
||||
"""
|
||||
return bool(FINGERPRINT_RE.match(value.strip()))
|
||||
|
||||
|
||||
def dearmor_key(module, b_data):
|
||||
"""
|
||||
Convert ASCII-armored key to binary format using gpg --dearmor.
|
||||
Returns binary key data.
|
||||
"""
|
||||
rc, stdout, stderr = module.run_command(
|
||||
['gpg', '--batch', '--yes', '--dearmor'],
|
||||
data=b_data,
|
||||
binary_data=True,
|
||||
encoding=None,
|
||||
)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f'Failed to dearmor key: {to_native(stderr)}')
|
||||
# stdout should be bytes with encoding=None
|
||||
return stdout
|
||||
|
||||
|
||||
def fetch_key_data(module, key):
|
||||
"""
|
||||
Fetch key data from URL, file path, or return raw PGP data.
|
||||
Returns bytes.
|
||||
"""
|
||||
# Check if it's a file path
|
||||
if os.path.isfile(key):
|
||||
with open(key, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
# Check if it's a URL
|
||||
parts = generic_urlparse(urlparse(key))
|
||||
if parts.scheme:
|
||||
try:
|
||||
r = open_url(key, http_agent=get_user_agent())
|
||||
return r.read()
|
||||
except Exception as exc:
|
||||
raise RuntimeError('Could not fetch signed_by key.') from exc
|
||||
|
||||
# Must be raw PGP data
|
||||
return to_bytes(key)
|
||||
|
||||
|
||||
def write_signed_by_key(module, v, slug):
|
||||
"""
|
||||
Process signing keys from a list.
|
||||
v: list of keys (URLs, file paths, fingerprints, or raw key data)
|
||||
Returns: (changed, signed_by_filename, signed_by_data, fingerprints)
|
||||
"""
|
||||
changed = False
|
||||
|
||||
# Separate fingerprints from other keys (URLs, files, PGP blocks)
|
||||
fingerprints = []
|
||||
other_keys = []
|
||||
for key in v:
|
||||
key = key.strip()
|
||||
if is_fingerprint(key):
|
||||
fingerprints.append(key)
|
||||
else:
|
||||
other_keys.append(key)
|
||||
|
||||
# If only fingerprints, return them directly (no file needed)
|
||||
if not other_keys:
|
||||
return changed, None, None, fingerprints
|
||||
|
||||
# Handle single inline PGP key ONLY if there are no fingerprints
|
||||
if len(other_keys) == 1 and not fingerprints and is_only_one_armored_block(other_keys[0]):
|
||||
return changed, None, other_keys[0], []
|
||||
|
||||
# For multiple keys, or when mixing with fingerprints, combine into a single file
|
||||
if len(other_keys) > 1 or fingerprints:
|
||||
all_key_data = []
|
||||
for key in other_keys:
|
||||
key_data = fetch_key_data(module, key)
|
||||
# Convert armored keys to binary for consistent format
|
||||
if is_armored(key_data):
|
||||
key_data = dearmor_key(module, key_data)
|
||||
all_key_data.append(key_data)
|
||||
combined_data = b''.join(all_key_data)
|
||||
|
||||
tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir)
|
||||
with os.fdopen(tmpfd, 'wb') as f:
|
||||
f.write(combined_data)
|
||||
|
||||
ext = 'gpg' # Keys are always saved as binary .gpg file
|
||||
filename = make_signed_by_filename(slug, ext)
|
||||
|
||||
src_chksum = module.sha256(tmpfile)
|
||||
dest_chksum = module.sha256(filename)
|
||||
|
||||
if src_chksum != dest_chksum:
|
||||
changed |= ensure_keyrings_dir(module)
|
||||
if not module.check_mode:
|
||||
module.atomic_move(tmpfile, filename)
|
||||
changed = True
|
||||
|
||||
changed |= module.set_mode_if_different(filename, S_IRWU_RG_RO, False)
|
||||
return changed, filename, None, fingerprints
|
||||
else:
|
||||
# Single key (URL or file path) with no fingerprints
|
||||
key_changed, signed_by_filename = process_single_key(module, other_keys[0], slug)
|
||||
return key_changed, signed_by_filename, None, []
|
||||
|
||||
|
||||
def process_single_key(module, v, slug):
|
||||
changed = False
|
||||
if os.path.isfile(v):
|
||||
return changed, v, None
|
||||
return changed, v
|
||||
|
||||
b_data = None
|
||||
|
||||
|
|
@ -358,11 +527,11 @@ def write_signed_by_key(module, v, slug):
|
|||
else:
|
||||
b_data = r.read()
|
||||
else:
|
||||
# Not a file, nor a URL, just pass it through
|
||||
return changed, None, v
|
||||
# Not a file, nor a URL, must be a fingerprint, just pass it through
|
||||
return changed, v
|
||||
|
||||
if not b_data:
|
||||
return changed, v, None
|
||||
return changed, v
|
||||
|
||||
tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir)
|
||||
with os.fdopen(tmpfd, 'wb') as f:
|
||||
|
|
@ -382,7 +551,7 @@ def write_signed_by_key(module, v, slug):
|
|||
|
||||
changed |= module.set_mode_if_different(filename, S_IRWU_RG_RO, False)
|
||||
|
||||
return changed, filename, None
|
||||
return changed, filename
|
||||
|
||||
|
||||
def install_python_debian(module, deb_pkg_name):
|
||||
|
|
@ -462,7 +631,8 @@ def main():
|
|||
'type': 'bool',
|
||||
},
|
||||
'signed_by': {
|
||||
'type': 'str',
|
||||
'type': 'list',
|
||||
'elements': 'str',
|
||||
},
|
||||
'suites': {
|
||||
'elements': 'str',
|
||||
|
|
@ -616,12 +786,26 @@ def main():
|
|||
value = format_bool(value)
|
||||
elif isinstance(value, int):
|
||||
value = to_native(value)
|
||||
elif key == 'signed_by':
|
||||
key_changed, signed_by_filename, signed_by_data, fingerprints = write_signed_by_key(module, value, slug)
|
||||
changed |= key_changed
|
||||
|
||||
# Build the Signed-By value
|
||||
if signed_by_data:
|
||||
# Single inline PGP key (no fingerprints in this case)
|
||||
value = signed_by_data
|
||||
else:
|
||||
# Combine keyring file path with fingerprints
|
||||
signed_by_parts = []
|
||||
if signed_by_filename:
|
||||
signed_by_parts.append(signed_by_filename)
|
||||
signed_by_parts.extend(fingerprints)
|
||||
value = ' '.join(signed_by_parts) if signed_by_parts else None
|
||||
|
||||
if value is None:
|
||||
continue
|
||||
elif is_sequence(value):
|
||||
value = format_list(value)
|
||||
elif key == 'signed_by':
|
||||
key_changed, signed_by_filename, signed_by_data = write_signed_by_key(module, value, slug)
|
||||
value = signed_by_filename or signed_by_data
|
||||
changed |= key_changed
|
||||
|
||||
if value.count('\n') > 0:
|
||||
value = format_multiline(value)
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@
|
|||
- remove_repos_1 is changed
|
||||
- remove_stats.results|map(attribute='stat')|selectattr('exists') == []
|
||||
|
||||
- name: Add repo with signed_by
|
||||
- name: Add repo with signed_by single key block
|
||||
deb822_repository:
|
||||
name: ansible-test
|
||||
types: deb
|
||||
|
|
@ -181,6 +181,95 @@
|
|||
signed_by: https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-example-binary.gpg
|
||||
register: signed_by_url
|
||||
|
||||
- name: Add repo with signed_by containing multiple keys - URLs and fingerprints
|
||||
deb822_repository:
|
||||
name: ansible-test
|
||||
types: deb
|
||||
uris: https://deb.debian.org
|
||||
suites: stable
|
||||
components:
|
||||
- main
|
||||
- contrib
|
||||
- non-free
|
||||
signed_by:
|
||||
- https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-example-binary.gpg
|
||||
- https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-example-binary.gpg
|
||||
- ffffDDDDeeee44449999nnnn2222ddddvvvvzzzz
|
||||
register: signed_by_multiple_keys
|
||||
|
||||
- name: Add repo with signed_by containing multiple keys - url, filepath, fingerprint
|
||||
deb822_repository:
|
||||
name: ansible-test
|
||||
types: deb
|
||||
uris: https://deb.debian.org
|
||||
suites: stable
|
||||
components:
|
||||
- main
|
||||
- contrib
|
||||
- non-free
|
||||
signed_by:
|
||||
- https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-example-binary.gpg
|
||||
- /etc/apt/keyrings/ansible-test.gpg
|
||||
- ffff DDDD eeee 4444 9999 nnnn 2222 dddd vvvv zzzz
|
||||
register: signed_by_multiple_keys_two
|
||||
|
||||
- name: Add repo with signed_by list of key blocks
|
||||
deb822_repository:
|
||||
name: ansible-test
|
||||
types: deb
|
||||
uris: https://deb.debian.org
|
||||
suites: stable
|
||||
components:
|
||||
- main
|
||||
- contrib
|
||||
- non-free
|
||||
signed_by:
|
||||
- |
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEYCQjIxYJKwYBBAHaRw8BAQdAD/P5Nvvnvk66SxBBHDbhRml9ORg1WV5CvzKY
|
||||
CuMfoIS0BmFiY2RlZoiQBBMWCgA4FiEErCIG1VhKWMWo2yfAREZd5NfO31cFAmAk
|
||||
IyMCGyMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQREZd5NfO31fbOwD6ArzS
|
||||
dM0Dkd5h2Ujy1b6KcAaVW9FOa5UNfJ9FFBtjLQEBAJ7UyWD3dZzhvlaAwunsk7DG
|
||||
3bHcln8DMpIJVXht78sL
|
||||
=IE0r
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
- |
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEYCQjIxYJKwYBBAHaRw8BAQdAD/P5Nvvnvk66SxBBHDbhRml9ORg1WV5CvzKY
|
||||
CuMfoIS0BmFiY2RlZoiQBBMWCgA4FiEErCIG1VhKWMWo2yfAREZd5NfO31cFAmAk
|
||||
IyMCGyMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQREZd5NfO31fbOwD6ArzS
|
||||
dM0Dkd5h2Ujy1b6KcAaVW9FOa5UNfJ9FFBtjLQEBAJ7UyWD3dZzhvlaAwunsk7DG
|
||||
3bHcln8DMpIJVXht78sL
|
||||
=IE0r
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
register: signed_by_multiple_key_blocks
|
||||
|
||||
- name: Add repo with signed_by as list with keyblock and url
|
||||
deb822_repository:
|
||||
name: ansible-test
|
||||
types: deb
|
||||
uris: https://deb.debian.org
|
||||
suites: stable
|
||||
components:
|
||||
- main
|
||||
- contrib
|
||||
- non-free
|
||||
signed_by:
|
||||
- |
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEYCQjIxYJKwYBBAHaRw8BAQdAD/P5Nvvnvk66SxBBHDbhRml9ORg1WV5CvzKY
|
||||
CuMfoIS0BmFiY2RlZoiQBBMWCgA4FiEErCIG1VhKWMWo2yfAREZd5NfO31cFAmAk
|
||||
IyMCGyMFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQREZd5NfO31fbOwD6ArzS
|
||||
dM0Dkd5h2Ujy1b6KcAaVW9FOa5UNfJ9FFBtjLQEBAJ7UyWD3dZzhvlaAwunsk7DG
|
||||
3bHcln8DMpIJVXht78sL
|
||||
=IE0r
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
- https://ci-files.testing.ansible.com/test/integration/targets/apt_key/apt-key-example-binary.gpg
|
||||
register: signed_by_multiple_keys_block_and_url
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- signed_by_inline.key_filename is none
|
||||
|
|
@ -189,6 +278,13 @@
|
|||
- signed_by_url.key_filename == '/etc/apt/keyrings/ansible-test.gpg'
|
||||
- >
|
||||
'BEGIN' not in signed_by_url.repo
|
||||
- signed_by_multiple_keys is changed
|
||||
- signed_by_multiple_keys.key_filename == '/etc/apt/keyrings/ansible-test.gpg'
|
||||
- signed_by_multiple_keys.repo|trim == signed_by_multiple_keys_expected
|
||||
- signed_by_multiple_keys_two.key_filename == '/etc/apt/keyrings/ansible-test.gpg'
|
||||
- signed_by_multiple_keys_two.repo|trim == signed_by_multiple_keys_expected
|
||||
- signed_by_multiple_key_blocks.key_filename == '/etc/apt/keyrings/ansible-test.gpg'
|
||||
- signed_by_multiple_keys_block_and_url.key_filename == '/etc/apt/keyrings/ansible-test.gpg'
|
||||
vars:
|
||||
signed_by_inline_expected: |-
|
||||
Components: main contrib non-free
|
||||
|
|
@ -206,6 +302,13 @@
|
|||
Suites: stable
|
||||
Types: deb
|
||||
URIs: https://deb.debian.org
|
||||
signed_by_multiple_keys_expected: |-
|
||||
Components: main contrib non-free
|
||||
X-Repolib-Name: ansible-test
|
||||
Signed-By: /etc/apt/keyrings/ansible-test.gpg
|
||||
Suites: stable
|
||||
Types: deb
|
||||
URIs: https://deb.debian.org
|
||||
|
||||
- name: remove ansible-test repo
|
||||
deb822_repository:
|
||||
|
|
|
|||
Loading…
Reference in a new issue