From 83af72a3bc7a50efbef6068def74a751455a15fa Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 27 Jun 2023 17:35:55 +0200 Subject: [PATCH] Improve PEM identification. (#628) --- changelogs/fragments/628-pem-detection.yml | 2 + plugins/module_utils/crypto/pem.py | 14 ++-- .../plugins/module_utils/crypto/test_pem.py | 67 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/628-pem-detection.yml create mode 100644 tests/unit/plugins/module_utils/crypto/test_pem.py diff --git a/changelogs/fragments/628-pem-detection.yml b/changelogs/fragments/628-pem-detection.yml new file mode 100644 index 00000000..a27bfd11 --- /dev/null +++ b/changelogs/fragments/628-pem-detection.yml @@ -0,0 +1,2 @@ +bugfixes: + - "Fix PEM detection/identification to also accept random other lines before the line starting with ``-----BEGIN`` (https://github.com/ansible-collections/community.crypto/issues/627, https://github.com/ansible-collections/community.crypto/pull/628)." diff --git a/plugins/module_utils/crypto/pem.py b/plugins/module_utils/crypto/pem.py index 4dc9745f..da46548c 100644 --- a/plugins/module_utils/crypto/pem.py +++ b/plugins/module_utils/crypto/pem.py @@ -14,10 +14,13 @@ PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY') PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' -def identify_pem_format(content): +def identify_pem_format(content, encoding='utf-8'): '''Given the contents of a binary file, tests whether this could be a PEM file.''' try: - lines = content.decode('utf-8').splitlines(False) + first_pem = extract_first_pem(content.decode(encoding)) + if first_pem is None: + return False + lines = first_pem.splitlines(False) if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): return True except UnicodeDecodeError: @@ -25,14 +28,17 @@ def identify_pem_format(content): return False -def identify_private_key_format(content): +def identify_private_key_format(content, encoding='utf-8'): '''Given the contents of a private key file, identifies its format.''' # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 # (PEM_read_bio_PrivateKey) # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47 # (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF) try: - lines = content.decode('utf-8').splitlines(False) + first_pem = extract_first_pem(content.decode(encoding)) + if first_pem is None: + return 'raw' + lines = first_pem.splitlines(False) if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): name = lines[0][len(PEM_START):-len(PEM_END)] if name in PKCS8_PRIVATEKEY_NAMES: diff --git a/tests/unit/plugins/module_utils/crypto/test_pem.py b/tests/unit/plugins/module_utils/crypto/test_pem.py new file mode 100644 index 00000000..183d81b9 --- /dev/null +++ b/tests/unit/plugins/module_utils/crypto/test_pem.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Felix Fontein +# 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 pytest + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_pem_format, + identify_private_key_format, + split_pem_list, + extract_first_pem, +) + + +PEM_TEST_CASES = [ + (b'', [], False, 'raw'), + (b'random stuff\nblabla', [], False, 'raw'), + (b'-----BEGIN PRIVATE KEY-----', [], False, 'raw'), + ( + b'-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----', + ['-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----'], + True, + 'pkcs8', + ), + ( + b'foo=bar\n# random stuff\n-----BEGIN RSA PRIVATE KEY-----\nblabla\n-----END RSA PRIVATE KEY-----\nmore stuff\n', + ['-----BEGIN RSA PRIVATE KEY-----\nblabla\n-----END RSA PRIVATE KEY-----\n'], + True, + 'pkcs1', + ), + ( + b'foo=bar\n# random stuff\n-----BEGIN CERTIFICATE-----\nblabla\n-----END CERTIFICATE-----\nmore stuff\n' + b'\n-----BEGIN CERTIFICATE-----\nfoobar\n-----END CERTIFICATE-----', + [ + '-----BEGIN CERTIFICATE-----\nblabla\n-----END CERTIFICATE-----\n', + '-----BEGIN CERTIFICATE-----\nfoobar\n-----END CERTIFICATE-----', + ], + True, + 'unknown-pem', + ), + ( + b'-----BEGINCERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n-----BEGINCERTIFICATE-----\n-----END CERTIFICATE-----\n-----BEGINCERTIFICATE-----\n', + [ + '-----BEGIN CERTIFICATE-----\n-----BEGINCERTIFICATE-----\n-----END CERTIFICATE-----\n', + ], + True, + 'unknown-pem', + ), +] + + +@pytest.mark.parametrize('data, pems, is_pem, private_key_type', PEM_TEST_CASES) +def test_pem_handling(data, pems, is_pem, private_key_type): + assert identify_pem_format(data) == is_pem + assert identify_private_key_format(data) == private_key_type + try: + text = data.decode('utf-8') + assert split_pem_list(text) == pems + first_pem = pems[0] if pems else None + assert extract_first_pem(text) == first_pem + except UnicodeDecodeError: + pass