mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Make all module_utils and plugin_utils private (#887)
* Add leading underscore. Remove deprecated module utils. * Document module and plugin utils as private. Add changelog fragment. * Convert relative to absolute imports. * Remove unnecessary imports.
This commit is contained in:
349
plugins/module_utils/_crypto/module_backends/privatekey_info.py
Normal file
349
plugins/module_utils/_crypto/module_backends/privatekey_info.py
Normal file
@@ -0,0 +1,349 @@
|
||||
# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
|
||||
# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
|
||||
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
|
||||
# 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
|
||||
|
||||
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
||||
# Do not use this from other collections or standalone plugins/modules!
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing as t
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native
|
||||
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
|
||||
OpenSSLObjectError,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._crypto.math import (
|
||||
binary_exp_mod,
|
||||
quick_is_not_prime,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.publickey_info import (
|
||||
_get_cryptography_public_key_info,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
|
||||
get_fingerprint_of_bytes,
|
||||
load_privatekey,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.module_utils._cryptography_dep import (
|
||||
COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
|
||||
assert_required_cryptography_version,
|
||||
)
|
||||
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.crypto.plugins.plugin_utils._action_module import (
|
||||
AnsibleActionModule,
|
||||
)
|
||||
from ansible_collections.community.crypto.plugins.plugin_utils._filter_module import (
|
||||
FilterModuleMock,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.types import (
|
||||
PrivateKeyTypes,
|
||||
)
|
||||
|
||||
GeneralAnsibleModule = t.Union[AnsibleModule, AnsibleActionModule, FilterModuleMock]
|
||||
|
||||
|
||||
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
SIGNATURE_TEST_DATA = b"1234"
|
||||
|
||||
|
||||
def _get_cryptography_private_key_info(
|
||||
key: PrivateKeyTypes, need_private_key_data: bool = False
|
||||
) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]:
|
||||
key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
|
||||
key_private_data: dict[str, t.Any] = {}
|
||||
if need_private_key_data:
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
rsa_private_numbers = key.private_numbers()
|
||||
key_private_data["p"] = rsa_private_numbers.p
|
||||
key_private_data["q"] = rsa_private_numbers.q
|
||||
key_private_data["exponent"] = rsa_private_numbers.d
|
||||
elif isinstance(
|
||||
key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey
|
||||
):
|
||||
dsa_private_numbers = key.private_numbers()
|
||||
key_private_data["x"] = dsa_private_numbers.x
|
||||
elif isinstance(
|
||||
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
|
||||
):
|
||||
ecc_private_numbers = key.private_numbers()
|
||||
key_private_data["multiplier"] = ecc_private_numbers.private_value
|
||||
return key_type, key_public_data, key_private_data
|
||||
|
||||
|
||||
def _check_dsa_consistency(
|
||||
key_public_data: dict[str, t.Any], key_private_data: dict[str, t.Any]
|
||||
) -> bool | None:
|
||||
# Get parameters
|
||||
p: int | None = key_public_data.get("p")
|
||||
if p is None:
|
||||
return None
|
||||
q: int | None = key_public_data.get("q")
|
||||
if q is None:
|
||||
return None
|
||||
g: int | None = key_public_data.get("g")
|
||||
if g is None:
|
||||
return None
|
||||
y: int | None = key_public_data.get("y")
|
||||
if y is None:
|
||||
return None
|
||||
x: int | None = key_private_data.get("x")
|
||||
if x is None:
|
||||
return None
|
||||
# Make sure that g is not 0, 1 or -1 in Z/pZ
|
||||
if g < 2 or g >= p - 1:
|
||||
return False
|
||||
# Make sure that x is in range
|
||||
if x < 1 or x >= q:
|
||||
return False
|
||||
# Check whether q divides p-1
|
||||
if (p - 1) % q != 0:
|
||||
return False
|
||||
# Check that g**q mod p == 1
|
||||
if binary_exp_mod(g, q, p) != 1:
|
||||
return False
|
||||
# Check whether g**x mod p == y
|
||||
if binary_exp_mod(g, x, p) != y:
|
||||
return False
|
||||
# Check (quickly) whether p or q are not primes
|
||||
if quick_is_not_prime(q) or quick_is_not_prime(p):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_cryptography_key_consistent(
|
||||
key: PrivateKeyTypes,
|
||||
key_public_data: dict[str, t.Any],
|
||||
key_private_data: dict[str, t.Any],
|
||||
warn_func: t.Callable[[str], None] | None = None,
|
||||
) -> bool | None:
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
# key._backend was removed in cryptography 42.0.0
|
||||
backend = getattr(key, "_backend", None)
|
||||
if backend is not None:
|
||||
return bool(backend._lib.RSA_check_key(key._rsa_cdata)) # type: ignore
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||
result = _check_dsa_consistency(key_public_data, key_private_data)
|
||||
if result is not None:
|
||||
return result
|
||||
signature = key.sign(
|
||||
SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256()
|
||||
)
|
||||
try:
|
||||
key.public_key().verify(
|
||||
signature,
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.hashes.SHA256(),
|
||||
)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
if isinstance(
|
||||
key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey
|
||||
):
|
||||
signature = key.sign(
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
|
||||
cryptography.hazmat.primitives.hashes.SHA256()
|
||||
),
|
||||
)
|
||||
try:
|
||||
key.public_key().verify(
|
||||
signature,
|
||||
SIGNATURE_TEST_DATA,
|
||||
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
|
||||
cryptography.hazmat.primitives.hashes.SHA256()
|
||||
),
|
||||
)
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
has_simple_sign_function = False
|
||||
if isinstance(
|
||||
key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey
|
||||
):
|
||||
has_simple_sign_function = True
|
||||
if isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||
has_simple_sign_function = True
|
||||
if has_simple_sign_function:
|
||||
signature = key.sign(SIGNATURE_TEST_DATA) # type: ignore
|
||||
try:
|
||||
key.public_key().verify(signature, SIGNATURE_TEST_DATA) # type: ignore
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature:
|
||||
return False
|
||||
# For X25519 and X448, there's no test yet.
|
||||
if warn_func is not None:
|
||||
warn_func(f"Cannot determine consistency for key of type {type(key)}")
|
||||
return None
|
||||
|
||||
|
||||
class PrivateKeyConsistencyError(OpenSSLObjectError):
|
||||
def __init__(self, msg: str, result: dict[str, t.Any]) -> None:
|
||||
super(PrivateKeyConsistencyError, self).__init__(msg)
|
||||
self.error_message = msg
|
||||
self.result = result
|
||||
|
||||
|
||||
class PrivateKeyParseError(OpenSSLObjectError):
|
||||
def __init__(self, msg: str, result: dict[str, t.Any]) -> None:
|
||||
super(PrivateKeyParseError, self).__init__(msg)
|
||||
self.error_message = msg
|
||||
self.result = result
|
||||
|
||||
|
||||
class PrivateKeyInfoRetrieval(metaclass=abc.ABCMeta):
|
||||
def __init__(
|
||||
self,
|
||||
module: GeneralAnsibleModule,
|
||||
content: bytes,
|
||||
passphrase: str | None = None,
|
||||
return_private_key_data: bool = False,
|
||||
check_consistency: bool = False,
|
||||
):
|
||||
self.module = module
|
||||
self.content = content
|
||||
self.passphrase = passphrase
|
||||
self.return_private_key_data = return_private_key_data
|
||||
self.check_consistency = check_consistency
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_public_key(self, binary: bool) -> bytes:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_key_info(
|
||||
self, need_private_key_data: bool = False
|
||||
) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def _is_key_consistent(
|
||||
self, key_public_data: dict[str, t.Any], key_private_data: dict[str, t.Any]
|
||||
) -> bool | None:
|
||||
pass
|
||||
|
||||
def get_info(self, prefer_one_fingerprint: bool = False) -> dict[str, t.Any]:
|
||||
result: dict[str, t.Any] = {
|
||||
"can_parse_key": False,
|
||||
"key_is_consistent": None,
|
||||
}
|
||||
priv_key_detail = self.content
|
||||
try:
|
||||
self.key = load_privatekey(
|
||||
path=None,
|
||||
content=priv_key_detail,
|
||||
passphrase=(
|
||||
to_bytes(self.passphrase)
|
||||
if self.passphrase is not None
|
||||
else self.passphrase
|
||||
),
|
||||
)
|
||||
result["can_parse_key"] = True
|
||||
except OpenSSLObjectError as exc:
|
||||
raise PrivateKeyParseError(str(exc), result)
|
||||
|
||||
result["public_key"] = to_native(self._get_public_key(binary=False))
|
||||
pk = self._get_public_key(binary=True)
|
||||
result["public_key_fingerprints"] = (
|
||||
get_fingerprint_of_bytes(pk, prefer_one=prefer_one_fingerprint)
|
||||
if pk is not None
|
||||
else dict()
|
||||
)
|
||||
|
||||
key_type, key_public_data, key_private_data = self._get_key_info(
|
||||
need_private_key_data=self.return_private_key_data or self.check_consistency
|
||||
)
|
||||
result["type"] = key_type
|
||||
result["public_data"] = key_public_data
|
||||
if self.return_private_key_data:
|
||||
result["private_data"] = key_private_data
|
||||
|
||||
if self.check_consistency:
|
||||
result["key_is_consistent"] = self._is_key_consistent(
|
||||
key_public_data, key_private_data
|
||||
)
|
||||
if result["key_is_consistent"] is False:
|
||||
# Only fail when it is False, to avoid to fail on None (which means "we do not know")
|
||||
msg = (
|
||||
"Private key is not consistent! (See "
|
||||
"https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
|
||||
)
|
||||
raise PrivateKeyConsistencyError(msg, result)
|
||||
return result
|
||||
|
||||
|
||||
class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
|
||||
"""Validate the supplied private key, using the cryptography backend"""
|
||||
|
||||
def __init__(self, module: GeneralAnsibleModule, content: bytes, **kwargs) -> None:
|
||||
super(PrivateKeyInfoRetrievalCryptography, self).__init__(
|
||||
module, content, **kwargs
|
||||
)
|
||||
|
||||
def _get_public_key(self, binary: bool) -> bytes:
|
||||
return self.key.public_key().public_bytes(
|
||||
serialization.Encoding.DER if binary else serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
|
||||
def _get_key_info(
|
||||
self, need_private_key_data: bool = False
|
||||
) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]:
|
||||
return _get_cryptography_private_key_info(
|
||||
self.key, need_private_key_data=need_private_key_data
|
||||
)
|
||||
|
||||
def _is_key_consistent(
|
||||
self, key_public_data: dict[str, t.Any], key_private_data: dict[str, t.Any]
|
||||
) -> bool | None:
|
||||
return _is_cryptography_key_consistent(
|
||||
self.key, key_public_data, key_private_data, warn_func=self.module.warn
|
||||
)
|
||||
|
||||
|
||||
def get_privatekey_info(
|
||||
module: GeneralAnsibleModule,
|
||||
content: bytes,
|
||||
passphrase: str | None = None,
|
||||
return_private_key_data: bool = False,
|
||||
prefer_one_fingerprint: bool = False,
|
||||
) -> dict[str, t.Any]:
|
||||
info = PrivateKeyInfoRetrievalCryptography(
|
||||
module,
|
||||
content,
|
||||
passphrase=passphrase,
|
||||
return_private_key_data=return_private_key_data,
|
||||
)
|
||||
return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
|
||||
|
||||
|
||||
def select_backend(
|
||||
module: GeneralAnsibleModule,
|
||||
content: bytes,
|
||||
passphrase: str | None = None,
|
||||
return_private_key_data: bool = False,
|
||||
check_consistency: bool = False,
|
||||
) -> PrivateKeyInfoRetrieval:
|
||||
assert_required_cryptography_version(
|
||||
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
|
||||
)
|
||||
return PrivateKeyInfoRetrievalCryptography(
|
||||
module,
|
||||
content,
|
||||
passphrase=passphrase,
|
||||
return_private_key_data=return_private_key_data,
|
||||
check_consistency=check_consistency,
|
||||
)
|
||||
Reference in New Issue
Block a user