diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index 228389367e8..84910105578 100644 --- a/.azure-pipelines/azure-pipelines.yml +++ b/.azure-pipelines/azure-pipelines.yml @@ -94,8 +94,8 @@ stages: test: rhel/9.6@3.9 - name: RHEL 9.6 py312 test: rhel/9.6@3.12 - - name: RHEL 10.0 - test: rhel/10.0 + - name: RHEL 10.1 + test: rhel/10.1 - name: FreeBSD 13.5 test: freebsd/13.5 - name: FreeBSD 14.3 @@ -110,8 +110,8 @@ stages: test: macos/15.3 - name: RHEL 9.6 test: rhel/9.6 - - name: RHEL 10.0 - test: rhel/10.0 + - name: RHEL 10.1 + test: rhel/10.1 groups: - 3 - 4 @@ -125,8 +125,8 @@ stages: test: fedora/42 - name: RHEL 9.6 test: rhel/9.6 - - name: RHEL 10.0 - test: rhel/10.0 + - name: RHEL 10.1 + test: rhel/10.1 - name: Ubuntu 24.04 test: ubuntu/24.04 groups: diff --git a/changelogs/fragments/86237-rpm-key-librpm.yml b/changelogs/fragments/86237-rpm-key-librpm.yml new file mode 100644 index 00000000000..b108b26298d --- /dev/null +++ b/changelogs/fragments/86237-rpm-key-librpm.yml @@ -0,0 +1,5 @@ +bugfixes: + - rpm_key - Use librpm library API instead of gpg utility to support version 6 PGP keys (https://github.com/ansible/ansible/issues/86157). + +minor_changes: + - ansible-test - Replace RHEL 10.0 with RHEL 10.1 as a remote platform for testing. diff --git a/lib/ansible/modules/rpm_key.py b/lib/ansible/modules/rpm_key.py index ced79688127..f7f36909d36 100644 --- a/lib/ansible/modules/rpm_key.py +++ b/lib/ansible/modules/rpm_key.py @@ -85,15 +85,308 @@ EXAMPLES = """ RETURN = r"""#""" +import ctypes +import ctypes.util +import hashlib import re import os.path import tempfile +import typing as t # import module snippets from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url from ansible.module_utils.common.text.converters import to_native +# Type alias for ctypes pointer to uint8 array (packet data) +# Using Any here because ctypes._Pointer is private, but documenting the actual type +PktPointer = t.Any # Actually: ctypes.POINTER(ctypes.c_uint8) + + +class LibRPM: + """Wrapper for librpm PGP key functions""" + + def __init__(self) -> None: + # Load the librpm library + lib_path = ctypes.util.find_library('rpm') + if not lib_path: + raise ImportError("Error: Could not find librpm library") + + try: + self.lib = ctypes.CDLL(lib_path) + except OSError: + raise ImportError(f"Error: Could not load librpm library from {lib_path}") + + try: + self.libc = ctypes.CDLL(None) + except OSError: + raise ImportError("Error: Could not load libC library") + + # Constants + self.PGPTAG_PUBLIC_KEY = 6 + self.PGPTAG_PUBLIC_SUBKEY = 14 + + # pgpArmor pgpParsePkts(const char *armor, uint8_t **pkt, size_t *pktlen) + self.lib.pgpParsePkts.argtypes = [ + ctypes.c_char_p, + ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), + ctypes.POINTER(ctypes.c_size_t) + ] + self.lib.pgpParsePkts.restype = ctypes.c_int + + # void free(void *ptr) + self.libc.free.argtypes = [ctypes.c_void_p] + self.libc.free.restype = None + + def _parse_armor(self, armor: str) -> tuple[t.Optional[PktPointer], int]: + """ + Parse ASCII armored PGP data using pgpParsePkts(). + Returns (pkt, pktlen) tuple or (None, 0) on error. + """ + pkt = ctypes.POINTER(ctypes.c_uint8)() + pktlen = ctypes.c_size_t() + + armor_bytes = armor.encode('utf-8') + result = self.lib.pgpParsePkts(armor_bytes, ctypes.byref(pkt), ctypes.byref(pktlen)) + + if result < 0 or not pkt: + return None, 0 + + return pkt, pktlen.value + + def _parse_packet_header(self, pkt: PktPointer, offset: int, pktlen: int) -> tuple[t.Optional[int], int, int]: + """ + Parse a PGP packet header to get tag and packet length. + Returns (tag, body_length, header_length) or (None, 0, 0) on error. + + Per RFC 9580 - Section 4.2: Packet Headers + https://www.rfc-editor.org/rfc/rfc9580.html#name-packet-headers + """ + if offset >= pktlen: + return None, 0, 0 + + tag_byte = pkt[offset] + + # Check if it's a new format packet (bit 6 set) + if tag_byte & 0x40: + # New format + tag = tag_byte & 0x3f # bits 0-5 are packet type ID + offset += 1 + + if offset >= pktlen: + return None, 0, 0 + + first_len_byte = pkt[offset] + + if first_len_byte < 192: + # One-octet length + return tag, first_len_byte, 2 + elif first_len_byte < 224: + # Two-octet length + if offset + 1 >= pktlen: + return None, 0, 0 + length = ((first_len_byte - 192) << 8) + pkt[offset + 1] + 192 + return tag, length, 3 + elif first_len_byte == 255: + # Five-octet length + if offset + 4 >= pktlen: + return None, 0, 0 + length = (pkt[offset + 1] << 24) | (pkt[offset + 2] << 16) | \ + (pkt[offset + 3] << 8) | pkt[offset + 4] + return tag, length, 6 + else: + # Partial body length (not supported here) + return None, 0, 0 + else: + # Old format + tag = (tag_byte >> 2) & 0x0f + length_type = tag_byte & 0x03 + + if length_type == 0: + # One-octet length + if offset + 1 >= pktlen: + return None, 0, 0 + return tag, pkt[offset + 1], 2 + elif length_type == 1: + # Two-octet length + if offset + 2 >= pktlen: + return None, 0, 0 + length = (pkt[offset + 1] << 8) | pkt[offset + 2] + return tag, length, 3 + elif length_type == 2: + # Four-octet length + if offset + 4 >= pktlen: + return None, 0, 0 + length = (pkt[offset + 1] << 24) | (pkt[offset + 2] << 16) | \ + (pkt[offset + 3] << 8) | pkt[offset + 4] + return tag, length, 5 + else: + # Indeterminate length (not supported) + return None, 0, 0 + + def _find_key_packets(self, pkt: PktPointer, pktlen: int) -> list[tuple[int, int]]: + """ + Walk the packet stream and find all PGPTAG_PUBLIC_KEY and PGPTAG_PUBLIC_SUBKEY packets. + Returns list of (offset, total_packet_length) tuples. + """ + key_packets: list[tuple[int, int]] = [] + offset = 0 + + while offset < pktlen: + tag, body_len, header_len = self._parse_packet_header(pkt, offset, pktlen) + + if tag is None: + break + + if tag in (self.PGPTAG_PUBLIC_KEY, self.PGPTAG_PUBLIC_SUBKEY): + # Found a key packet + total_len = header_len + body_len + key_packets.append((offset, total_len)) + + # Move to next packet + offset += header_len + body_len + + return key_packets + + def _get_key_version(self, pkt: PktPointer, offset: int, pktlen: int) -> t.Optional[int]: + """ + Get the version byte from a key packet. + Returns version number (4 or 6) or None on error. + """ + tag, dummy, header_len = self._parse_packet_header(pkt, offset, pktlen) + if tag is None: + return None + + # Extract packet body (skip the packet header) + body_offset = offset + header_len + if body_offset >= pktlen: + return None + + # First byte of body is the version + return pkt[body_offset] + + def _compute_v4_fingerprint(self, pkt: PktPointer, offset: int, pktlen: int) -> t.Optional[str]: + """ + Compute V4 fingerprint from packet data. + For V4 keys, fingerprint = SHA-1(0x99 || 2-byte-length || packet_body) + Per RFC 4880 Section 12.2 + """ + tag, body_len, header_len = self._parse_packet_header(pkt, offset, pktlen) + + if tag is None: + return None + + # Extract packet body (skip the packet header) + body_offset = offset + header_len + if body_offset + body_len > pktlen: + return None + + # Check if it's a V4 key (first byte of body should be 0x04) + if pkt[body_offset] != 0x04: + return None + + # Build the data for fingerprint: 0x99 || 2-byte length || body + fp_data = bytearray() + fp_data.append(0x99) # V4 public key packet tag + fp_data.append((body_len >> 8) & 0xFF) # Length high byte + fp_data.append(body_len & 0xFF) # Length low byte + + # Append the packet body + for i in range(body_len): + fp_data.append(pkt[body_offset + i]) + + # Compute SHA-1 hash + fingerprint = hashlib.sha1(fp_data).digest() + return fingerprint.hex().upper() + + def _compute_v6_fingerprint(self, pkt: PktPointer, offset: int, pktlen: int) -> t.Optional[str]: + """ + Compute V6 fingerprint from packet data. + For V6 keys, fingerprint = SHA-256(0x9B || 4-byte-length || packet_body) + Per RFC 9580 Section 5.5.4 + """ + tag, body_len, header_len = self._parse_packet_header(pkt, offset, pktlen) + + if tag is None: + return None + + # Extract packet body (skip the packet header) + body_offset = offset + header_len + if body_offset + body_len > pktlen: + return None + + # Check if it's a V6 key (first byte of body should be 0x06) + if pkt[body_offset] != 0x06: + return None + + # Build the data for fingerprint: 0x9B || 4-byte length || body + fp_data = bytearray() + fp_data.append(0x9B) # V6 public key packet tag + fp_data.append((body_len >> 24) & 0xFF) # Length byte 1 (MSB) + fp_data.append((body_len >> 16) & 0xFF) # Length byte 2 + fp_data.append((body_len >> 8) & 0xFF) # Length byte 3 + fp_data.append(body_len & 0xFF) # Length byte 4 (LSB) + + # Append the packet body + for i in range(body_len): + fp_data.append(pkt[body_offset + i]) + + # Compute SHA-256 hash + fingerprint = hashlib.sha256(fp_data).digest() + return fingerprint.hex().upper() + + def _identify_keys(self, armor: str) -> list[dict[str, str]]: + """Return a list of dicts with key ID and fingerprint for the primary key and each subkey""" + key_info: list[dict[str, str]] = [] + + pkt, pktlen = self._parse_armor(armor) + if not pkt: + raise Exception("Unable to parse PGP key armor") + + # Find all key packets in the stream and compute their fingerprints. + key_packets = self._find_key_packets(pkt, pktlen) + + for offset, dummy in key_packets: + # Detect key version + version = self._get_key_version(pkt, offset, pktlen) + + if version == 0x04: + # V4 key + computed_fp = self._compute_v4_fingerprint(pkt, offset, pktlen) + if computed_fp: + # V4: Key ID is the last 8 bytes (16 hex chars) of the fingerprint + keyid_from_fp = computed_fp[-16:] + key_info.append({'keyid': keyid_from_fp, 'fingerprint': computed_fp}) + elif version == 0x06: + # V6 key + computed_fp = self._compute_v6_fingerprint(pkt, offset, pktlen) + if computed_fp: + # V6: Key ID is the first 8 bytes (16 hex chars) of the fingerprint + keyid_from_fp = computed_fp[:16] + key_info.append({'keyid': keyid_from_fp, 'fingerprint': computed_fp}) + + self.libc.free(pkt) + + return key_info + + def get_key_ids_from_armor(self, armor: str) -> list[str]: + """ + Get the key IDs from the primary PGP key, and all subkeys of that key, from the ASCII armored key. + + 'armor' is expected to be a single ASCII armored PGP key (v4 or v6). The primary key should be the + first item in the results, followed by its subkeys. + """ + return [key['keyid'] for key in self._identify_keys(armor)] + + def get_fingerprints_from_armor(self, armor: str) -> list[str]: + """ + Get the fingerprints from the primary PGP key, and all subkeys of that key, from the ASCII armored key. + + 'armor' is expected to be a single ASCII armored PGP key (v4 or v6). The primary key should be the + first item in the results, followed by its subkeys. + """ + return [key['fingerprint'] for key in self._identify_keys(armor)] + def is_pubkey(string): """Verifies if string is a pubkey""" @@ -120,9 +413,7 @@ class RpmKey(object): fingerprint = [fingerprint] fingerprints = set(f.replace(' ', '').upper() for f in fingerprint) - self.gpg = self.module.get_bin_path('gpg') - if not self.gpg: - self.gpg = self.module.get_bin_path('gpg2', required=True) + self.librpm = LibRPM() if '://' in key: keyfile = self.fetch_key(key) @@ -187,40 +478,18 @@ class RpmKey(object): return ret def getkeyid(self, keyfile): - stdout, stderr = self.execute_command([self.gpg, '--no-tty', '--batch', '--with-colons', '--fixed-list-mode', keyfile]) - for line in stdout.splitlines(): - line = line.strip() - if line.startswith('pub:'): - return line.split(':')[4] - - self.module.fail_json(msg="Unexpected gpg output") + with open(keyfile, "r") as key_fd: + key_ids = self.librpm.get_key_ids_from_armor(key_fd.read()) + if not key_ids: + self.module.fail_json(msg="Failed to get keyid") + return key_ids[0] def getfingerprints(self, keyfile): - stdout, stderr = self.execute_command([ - self.gpg, '--no-tty', '--batch', '--with-colons', - '--fixed-list-mode', '--import', '--import-options', 'show-only', - '--dry-run', keyfile - ]) - - fingerprints = set() - - for line in stdout.splitlines(): - line = line.strip() - if line.startswith('fpr:'): - # As mentioned here, - # - # https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS - # - # The description of the `fpr` field says - # - # "fpr :: Fingerprint (fingerprint is in field 10)" - # - fingerprints.add(line.split(':')[9]) - - if fingerprints: - return fingerprints - - self.module.fail_json(msg="Unexpected gpg output") + with open(keyfile, "r") as key_fd: + fingerprints = self.librpm.get_fingerprints_from_armor(key_fd.read()) + if not fingerprints: + self.module.fail_json(msg="Failed to get fingerprint") + return frozenset(fingerprints) def is_keyid(self, keystr): """Verifies if a key, as provided by the user is a keyid""" @@ -233,15 +502,39 @@ class RpmKey(object): return stdout, stderr def is_key_imported(self, keyid): + """ + Uses 'rpm' CLI to output the ASCII armor of all imported keys, then gets the key ID + for each to determine if the supplied key is among them. + """ cmd = self.rpm + ' -q gpg-pubkey' rc, stdout, stderr = self.module.run_command(cmd) if rc != 0: # No key is installed on system return False - cmd += ' --qf "%{description}" | ' + self.gpg + ' --no-tty --batch --with-colons --fixed-list-mode -' + cmd += ' --qf "%{description}"' stdout, stderr = self.execute_command(cmd) + + # Split the content into individual key blocks + key_blocks = [] + current_block = [] + in_key_block = False + for line in stdout.splitlines(): - if keyid in line.split(':')[4]: + if line.strip() == '-----BEGIN PGP PUBLIC KEY BLOCK-----': + in_key_block = True + current_block = [line] + elif line.strip() == '-----END PGP PUBLIC KEY BLOCK-----': + current_block.append(line) + key_blocks.append('\n'.join(current_block)) + current_block = [] + in_key_block = False + elif in_key_block: + current_block.append(line) + + for armor_string in key_blocks: + key_ids = self.librpm.get_key_ids_from_armor(armor_string) + if keyid in key_ids: return True + return False def import_key(self, keyfile): diff --git a/test/integration/targets/rpm_key/files/test-v6-key.cert b/test/integration/targets/rpm_key/files/test-v6-key.cert new file mode 100644 index 00000000000..c44fa2e68be --- /dev/null +++ b/test/integration/targets/rpm_key/files/test-v6-key.cert @@ -0,0 +1,521 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: 3F88 FFB5 8C58 7323 B818 9B81 DF71 002F FA1B 4101 CDDA 432E BF34 21D5 67E2 5C5E +Comment: + +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 +EhYbIicz +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/integration/targets/rpm_key/tasks/main.yaml b/test/integration/targets/rpm_key/tasks/main.yaml index 3da5bfc4e6e..d4d2635514a 100644 --- a/test/integration/targets/rpm_key/tasks/main.yaml +++ b/test/integration/targets/rpm_key/tasks/main.yaml @@ -19,8 +19,10 @@ always: + # This will fail if the tests leave no keys in the key store - name: Remove all GPG keys from key ring shell: rpm -q gpg-pubkey | xargs rpm -e + ignore_errors: yes - name: Restore the previously installed GPG keys command: rpm --import {{ (remote_tmp_dir + '/pubkeys') | quote }} diff --git a/test/integration/targets/rpm_key/tasks/rpm_key.yaml b/test/integration/targets/rpm_key/tasks/rpm_key.yaml index f83da4fcca1..d1951b71d31 100644 --- a/test/integration/targets/rpm_key/tasks/rpm_key.yaml +++ b/test/integration/targets/rpm_key/tasks/rpm_key.yaml @@ -102,21 +102,17 @@ assert: that: checksig.stdout is contains 'digests signatures OK' -- name: Get keyid - shell: rpm -q gpg-pubkey | head -n 1 | xargs rpm -q --qf %{version} - register: key_id - - name: Remove test key using keyid rpm_key: state: absent - key: "{{ key_id.stdout }}" + key: "{{ test_key_keyid }}" register: remove_keyid failed_when: not remove_keyid.changed - name: Remove test key using keyid (idempotent) rpm_key: state: absent - key: "{{ key_id.stdout }}" + key: "{{ test_key_keyid }}" register: key_id_idempotence - name: Verify idempotent (key_id) @@ -227,3 +223,56 @@ that: - result is success - result is not changed + +# Test PGPv6 keys +- name: Test PGPv6 Keys + block: + + # Reset to test PGPv6 keys validation + - name: Remove all keys from key ring + shell: rpm -q gpg-pubkey | xargs rpm -e + + - name: Copy v6 key to remote + copy: + src: "files/{{ test_v6_key_file }}" + dest: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}" + mode: "0644" + + - name: Import v6 key + rpm_key: + state: present + key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}" + fingerprint: "{{ invalid_fingerprint }}" + register: result + failed_when: result is success + + - name: Verify invalid fingerprint failure + assert: + that: + - result is success + - result is not changed + - result.msg is contains 'does not match any key fingerprints' + + - name: Import v6 key + rpm_key: + state: present + key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}" + fingerprint: "{{ test_v6_key_fingerprint }}" + + - name: Import v6 key (idempotent) + rpm_key: + state: present + key: "{{ remote_tmp_dir }}/{{ test_v6_key_file }}" + fingerprint: "{{ test_v6_key_fingerprint }}" + register: key_idempotence + + - name: Verify idempotence + assert: + that: key_idempotence is not changed + + - name: Delete v6 key + rpm_key: + state: absent + key: "{{ test_v6_key_keyid }}" + + when: ansible_facts['distribution'] == "RedHat" and ansible_facts['distribution_version'] is version('10.1', '>=') diff --git a/test/integration/targets/rpm_key/vars/main.yml b/test/integration/targets/rpm_key/vars/main.yml index 694a6d0c7f4..e29a1d27026 100644 --- a/test/integration/targets/rpm_key/vars/main.yml +++ b/test/integration/targets/rpm_key/vars/main.yml @@ -1,4 +1,5 @@ test_key_url: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/RPM-GPG-KEY-EPEL-10 +test_key_keyid: 33D98517E37ED158 test_key_path: "{{ remote_tmp_dir + '/RPM-GPG-KEY-EPEL-10' }}" test_rpm_url: https://ci-files.testing.ansible.com/test/integration/targets/rpm_key/scitokens-cpp-devel-1.1.3-2.el10_1.x86_64.rpm test_rpm_path: "{{ remote_tmp_dir + '/test.rpm' }}" @@ -6,3 +7,6 @@ sub_key_url: https://ci-files.testing.ansible.com/test/integration/targets/rpm_k invalid_fingerprint: 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 primary_fingerprint: 66D1 5FDD 8728 7219 C8E1 5478 D200 CD70 2853 E6D0 sub_key_fingerprint: E617 DCD4 065C 2AFC 0B2C F7A7 BA8B C08C 0F69 1F94 +test_v6_key_file: "test-v6-key.cert" +test_v6_key_fingerprint: "3F88FFB58C587323B8189B81DF71002FFA1B4101CDDA432EBF3421D567E25C5E" +test_v6_key_keyid: "3F88FFB58C587323" diff --git a/test/lib/ansible_test/_data/completion/remote.txt b/test/lib/ansible_test/_data/completion/remote.txt index 7405c369889..5e5e272ad21 100644 --- a/test/lib/ansible_test/_data/completion/remote.txt +++ b/test/lib/ansible_test/_data/completion/remote.txt @@ -8,7 +8,7 @@ freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64 macos/15.3 python=3.13 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64 macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64 rhel/9.6 python=3.9,3.12 become=sudo provider=aws arch=x86_64 -rhel/10.0 python=3.12 become=sudo provider=aws arch=x86_64 +rhel/10.1 python=3.12 become=sudo provider=aws arch=x86_64 rhel become=sudo provider=aws arch=x86_64 ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64 ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64