mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-03-26 21:33:25 +00:00
944 lines
38 KiB
Python
944 lines
38 KiB
Python
# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
|
# 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 binascii
|
|
import typing as t
|
|
|
|
from ansible.module_utils.common.text.converters import to_text
|
|
from ansible_collections.community.crypto.plugins.module_utils._argspec import (
|
|
ArgumentSpec,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
|
|
OpenSSLBadPassphraseError,
|
|
OpenSSLObjectError,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_crl import (
|
|
REVOCATION_REASON_MAP,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
|
|
cryptography_get_basic_constraints,
|
|
cryptography_get_name,
|
|
cryptography_key_needs_digest_for_signing,
|
|
cryptography_name_to_oid,
|
|
cryptography_parse_key_usage_params,
|
|
cryptography_parse_relative_distinguished_name,
|
|
is_potential_certificate_issuer_public_key,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._crypto.module_backends.csr_info import (
|
|
get_csr_info,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
|
|
load_certificate_issuer_privatekey,
|
|
load_certificate_request,
|
|
parse_name_field,
|
|
parse_ordered_name_field,
|
|
select_message_digest,
|
|
)
|
|
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.module_utils._crypto.cryptography_support import (
|
|
CertificatePrivateKeyTypes,
|
|
)
|
|
from cryptography.hazmat.primitives.asymmetric.types import (
|
|
CertificateIssuerPrivateKeyTypes,
|
|
PrivateKeyTypes,
|
|
)
|
|
|
|
_ET = t.TypeVar("_ET", bound="cryptography.x509.ExtensionType")
|
|
|
|
|
|
MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION
|
|
|
|
try:
|
|
import cryptography
|
|
import cryptography.exceptions
|
|
import cryptography.hazmat.backends
|
|
import cryptography.hazmat.primitives.hashes
|
|
import cryptography.hazmat.primitives.serialization
|
|
import cryptography.x509
|
|
import cryptography.x509.oid
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
class CertificateSigningRequestError(OpenSSLObjectError):
|
|
pass
|
|
|
|
|
|
# From the object called `module`, only the following properties are used:
|
|
#
|
|
# - module.params[]
|
|
# - module.warn(msg: str)
|
|
# - module.fail_json(msg: str, **kwargs)
|
|
|
|
|
|
class CertificateSigningRequestBackend(metaclass=abc.ABCMeta):
|
|
def __init__(self, *, module: AnsibleModule) -> None:
|
|
self.module = module
|
|
self.digest: str = module.params["digest"]
|
|
self.privatekey_path: str | None = module.params["privatekey_path"]
|
|
privatekey_content: str | None = module.params["privatekey_content"]
|
|
if privatekey_content is not None:
|
|
self.privatekey_content: bytes | None = privatekey_content.encode("utf-8")
|
|
else:
|
|
self.privatekey_content = None
|
|
self.privatekey_passphrase: str | None = module.params["privatekey_passphrase"]
|
|
self.version: t.Literal[1] = module.params["version"]
|
|
self.subject_alt_name: list[str] | None = module.params["subject_alt_name"]
|
|
self.subject_alt_name_critical: bool = module.params[
|
|
"subject_alt_name_critical"
|
|
]
|
|
self.key_usage: list[str] | None = module.params["key_usage"]
|
|
self.key_usage_critical: bool = module.params["key_usage_critical"]
|
|
self.extended_key_usage: list[str] | None = module.params["extended_key_usage"]
|
|
self.extended_key_usage_critical: bool = module.params[
|
|
"extended_key_usage_critical"
|
|
]
|
|
self.basic_constraints: list[str] | None = module.params["basic_constraints"]
|
|
self.basic_constraints_critical: bool = module.params[
|
|
"basic_constraints_critical"
|
|
]
|
|
self.ocsp_must_staple: bool = module.params["ocsp_must_staple"]
|
|
self.ocsp_must_staple_critical: bool = module.params[
|
|
"ocsp_must_staple_critical"
|
|
]
|
|
self.name_constraints_permitted: list[str] = (
|
|
module.params["name_constraints_permitted"] or []
|
|
)
|
|
self.name_constraints_excluded: list[str] = (
|
|
module.params["name_constraints_excluded"] or []
|
|
)
|
|
self.name_constraints_critical: bool = module.params[
|
|
"name_constraints_critical"
|
|
]
|
|
self.create_subject_key_identifier: bool = module.params[
|
|
"create_subject_key_identifier"
|
|
]
|
|
subject_key_identifier: str | None = module.params["subject_key_identifier"]
|
|
authority_key_identifier: str | None = module.params["authority_key_identifier"]
|
|
self.authority_cert_issuer: list[str] | None = module.params[
|
|
"authority_cert_issuer"
|
|
]
|
|
self.authority_cert_serial_number: int = module.params[
|
|
"authority_cert_serial_number"
|
|
]
|
|
self.crl_distribution_points: (
|
|
list[cryptography.x509.DistributionPoint] | None
|
|
) = None
|
|
self.csr: cryptography.x509.CertificateSigningRequest | None = None
|
|
self.privatekey: CertificateIssuerPrivateKeyTypes | None = None
|
|
|
|
if self.create_subject_key_identifier and subject_key_identifier is not None:
|
|
module.fail_json(
|
|
msg="subject_key_identifier cannot be specified if create_subject_key_identifier is true"
|
|
)
|
|
|
|
self.ordered_subject = False
|
|
subject = [
|
|
("C", module.params["country_name"]),
|
|
("ST", module.params["state_or_province_name"]),
|
|
("L", module.params["locality_name"]),
|
|
("O", module.params["organization_name"]),
|
|
("OU", module.params["organizational_unit_name"]),
|
|
("CN", module.params["common_name"]),
|
|
("emailAddress", module.params["email_address"]),
|
|
]
|
|
self.subject: list[tuple[str, str]] = [
|
|
(entry[0], entry[1]) for entry in subject if entry[1]
|
|
]
|
|
|
|
try:
|
|
if module.params["subject"]:
|
|
self.subject = self.subject + parse_name_field(
|
|
module.params["subject"], name_field_name="subject"
|
|
)
|
|
if module.params["subject_ordered"]:
|
|
if self.subject:
|
|
raise CertificateSigningRequestError(
|
|
"subject_ordered cannot be combined with any other subject field"
|
|
)
|
|
self.subject = parse_ordered_name_field(
|
|
module.params["subject_ordered"], name_field_name="subject_ordered"
|
|
)
|
|
self.ordered_subject = True
|
|
except ValueError as exc:
|
|
raise CertificateSigningRequestError(str(exc)) from exc
|
|
|
|
self.using_common_name_for_san = False
|
|
if not self.subject_alt_name and module.params["use_common_name_for_san"]:
|
|
for sub in self.subject:
|
|
if sub[0] in ("commonName", "CN"):
|
|
self.subject_alt_name = [f"DNS:{sub[1]}"]
|
|
self.using_common_name_for_san = True
|
|
break
|
|
|
|
self.subject_key_identifier: bytes | None = None
|
|
if subject_key_identifier is not None:
|
|
try:
|
|
self.subject_key_identifier = binascii.unhexlify(
|
|
subject_key_identifier.replace(":", "")
|
|
)
|
|
except Exception as e:
|
|
raise CertificateSigningRequestError(
|
|
f"Cannot parse subject_key_identifier: {e}"
|
|
) from e
|
|
|
|
self.authority_key_identifier: bytes | None = None
|
|
if authority_key_identifier is not None:
|
|
try:
|
|
self.authority_key_identifier = binascii.unhexlify(
|
|
authority_key_identifier.replace(":", "")
|
|
)
|
|
except Exception as e:
|
|
raise CertificateSigningRequestError(
|
|
f"Cannot parse authority_key_identifier: {e}"
|
|
) from e
|
|
|
|
self.existing_csr: cryptography.x509.CertificateSigningRequest | None = None
|
|
self.existing_csr_bytes: bytes | None = None
|
|
|
|
self.diff_before = self._get_info(data=None)
|
|
self.diff_after = self._get_info(data=None)
|
|
|
|
def _get_info(self, *, data: bytes | None) -> dict[str, t.Any]:
|
|
if data is None:
|
|
return {}
|
|
try:
|
|
result = get_csr_info(
|
|
module=self.module,
|
|
content=data,
|
|
validate_signature=False,
|
|
prefer_one_fingerprint=True,
|
|
)
|
|
result["can_parse_csr"] = True
|
|
return result
|
|
except Exception:
|
|
return {"can_parse_csr": False}
|
|
|
|
@abc.abstractmethod
|
|
def generate_csr(self) -> None:
|
|
"""(Re-)Generate CSR."""
|
|
|
|
@abc.abstractmethod
|
|
def get_csr_data(self) -> bytes:
|
|
"""Return bytes for self.csr."""
|
|
|
|
def set_existing(self, *, csr_bytes: bytes | None) -> None:
|
|
"""Set existing CSR bytes. None indicates that the CSR does not exist."""
|
|
self.existing_csr_bytes = csr_bytes
|
|
self.diff_after = self.diff_before = self._get_info(
|
|
data=self.existing_csr_bytes
|
|
)
|
|
|
|
def has_existing(self) -> bool:
|
|
"""Query whether an existing CSR is/has been there."""
|
|
return self.existing_csr_bytes is not None
|
|
|
|
def _ensure_private_key_loaded(self) -> None:
|
|
"""Load the provided private key into self.privatekey."""
|
|
if self.privatekey is not None:
|
|
return
|
|
try:
|
|
self.privatekey = load_certificate_issuer_privatekey(
|
|
path=self.privatekey_path,
|
|
content=self.privatekey_content,
|
|
passphrase=self.privatekey_passphrase,
|
|
)
|
|
except OpenSSLBadPassphraseError as exc:
|
|
raise CertificateSigningRequestError(exc) from exc
|
|
|
|
@abc.abstractmethod
|
|
def _check_csr(self) -> bool:
|
|
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
|
|
|
|
def needs_regeneration(self) -> bool:
|
|
"""Check whether a regeneration is necessary."""
|
|
if self.existing_csr_bytes is None:
|
|
return True
|
|
try:
|
|
self.existing_csr = load_certificate_request(
|
|
content=self.existing_csr_bytes,
|
|
)
|
|
except Exception:
|
|
return True
|
|
self._ensure_private_key_loaded()
|
|
return not self._check_csr()
|
|
|
|
def dump(self, *, include_csr: bool) -> dict[str, t.Any]:
|
|
"""Serialize the object into a dictionary."""
|
|
result: dict[str, t.Any] = {
|
|
"privatekey": self.privatekey_path,
|
|
"subject": self.subject,
|
|
"subjectAltName": self.subject_alt_name,
|
|
"keyUsage": self.key_usage,
|
|
"extendedKeyUsage": self.extended_key_usage,
|
|
"basicConstraints": self.basic_constraints,
|
|
"ocspMustStaple": self.ocsp_must_staple,
|
|
"name_constraints_permitted": self.name_constraints_permitted,
|
|
"name_constraints_excluded": self.name_constraints_excluded,
|
|
}
|
|
# Get hold of CSR bytes
|
|
csr_bytes = self.existing_csr_bytes
|
|
if self.csr is not None:
|
|
csr_bytes = self.get_csr_data()
|
|
self.diff_after = self._get_info(data=csr_bytes)
|
|
if include_csr:
|
|
# Store result
|
|
result["csr"] = csr_bytes.decode("utf-8") if csr_bytes else None
|
|
|
|
result["diff"] = {
|
|
"before": self.diff_before,
|
|
"after": self.diff_after,
|
|
}
|
|
return result
|
|
|
|
|
|
def parse_crl_distribution_points(
|
|
*, module: AnsibleModule, crl_distribution_points: list[dict[str, t.Any]]
|
|
) -> list[cryptography.x509.DistributionPoint]:
|
|
result = []
|
|
for index, parse_crl_distribution_point in enumerate(crl_distribution_points):
|
|
try:
|
|
full_name = None
|
|
relative_name = None
|
|
crl_issuer = None
|
|
reasons = None
|
|
if parse_crl_distribution_point["full_name"] is not None:
|
|
if not parse_crl_distribution_point["full_name"]:
|
|
raise OpenSSLObjectError("full_name must not be empty")
|
|
full_name = [
|
|
cryptography_get_name(name, what="full name")
|
|
for name in parse_crl_distribution_point["full_name"]
|
|
]
|
|
if parse_crl_distribution_point["relative_name"] is not None:
|
|
if not parse_crl_distribution_point["relative_name"]:
|
|
raise OpenSSLObjectError("relative_name must not be empty")
|
|
relative_name = cryptography_parse_relative_distinguished_name(
|
|
parse_crl_distribution_point["relative_name"]
|
|
)
|
|
if parse_crl_distribution_point["crl_issuer"] is not None:
|
|
if not parse_crl_distribution_point["crl_issuer"]:
|
|
raise OpenSSLObjectError("crl_issuer must not be empty")
|
|
crl_issuer = [
|
|
cryptography_get_name(name, what="CRL issuer")
|
|
for name in parse_crl_distribution_point["crl_issuer"]
|
|
]
|
|
if parse_crl_distribution_point["reasons"] is not None:
|
|
reasons_list = []
|
|
for reason in parse_crl_distribution_point["reasons"]:
|
|
reasons_list.append(REVOCATION_REASON_MAP[reason])
|
|
reasons = frozenset(reasons_list)
|
|
result.append(
|
|
cryptography.x509.DistributionPoint(
|
|
full_name=full_name,
|
|
relative_name=relative_name,
|
|
crl_issuer=crl_issuer,
|
|
reasons=reasons,
|
|
)
|
|
)
|
|
except (OpenSSLObjectError, ValueError) as e:
|
|
raise OpenSSLObjectError(
|
|
f"Error while parsing CRL distribution point #{index}: {e}"
|
|
) from e
|
|
return result
|
|
|
|
|
|
# Implementation with using cryptography
|
|
class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBackend):
|
|
def __init__(self, *, module: AnsibleModule) -> None:
|
|
super().__init__(module=module)
|
|
if self.version != 1:
|
|
module.warn(
|
|
"The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)"
|
|
)
|
|
|
|
crl_distribution_points: list[dict[str, t.Any]] | None = module.params[
|
|
"crl_distribution_points"
|
|
]
|
|
if crl_distribution_points:
|
|
self.crl_distribution_points = parse_crl_distribution_points(
|
|
module=module, crl_distribution_points=crl_distribution_points
|
|
)
|
|
|
|
def generate_csr(self) -> None:
|
|
"""(Re-)Generate CSR."""
|
|
self._ensure_private_key_loaded()
|
|
assert self.privatekey is not None
|
|
|
|
csr = cryptography.x509.CertificateSigningRequestBuilder()
|
|
try:
|
|
csr = csr.subject_name(
|
|
cryptography.x509.Name(
|
|
[
|
|
cryptography.x509.NameAttribute(
|
|
cryptography_name_to_oid(entry[0]), to_text(entry[1])
|
|
)
|
|
for entry in self.subject
|
|
]
|
|
)
|
|
)
|
|
except ValueError as e:
|
|
raise CertificateSigningRequestError(e) from e
|
|
|
|
if self.subject_alt_name:
|
|
csr = csr.add_extension(
|
|
cryptography.x509.SubjectAlternativeName(
|
|
[cryptography_get_name(name) for name in self.subject_alt_name]
|
|
),
|
|
critical=self.subject_alt_name_critical,
|
|
)
|
|
|
|
if self.key_usage:
|
|
params = cryptography_parse_key_usage_params(self.key_usage)
|
|
csr = csr.add_extension(
|
|
cryptography.x509.KeyUsage(**params), critical=self.key_usage_critical
|
|
)
|
|
|
|
if self.extended_key_usage:
|
|
usages = [
|
|
cryptography_name_to_oid(usage) for usage in self.extended_key_usage
|
|
]
|
|
csr = csr.add_extension(
|
|
cryptography.x509.ExtendedKeyUsage(usages),
|
|
critical=self.extended_key_usage_critical,
|
|
)
|
|
|
|
if self.basic_constraints:
|
|
params = {}
|
|
ca, path_length = cryptography_get_basic_constraints(self.basic_constraints)
|
|
csr = csr.add_extension(
|
|
cryptography.x509.BasicConstraints(ca, path_length),
|
|
critical=self.basic_constraints_critical,
|
|
)
|
|
|
|
if self.ocsp_must_staple:
|
|
csr = csr.add_extension(
|
|
cryptography.x509.TLSFeature(
|
|
[cryptography.x509.TLSFeatureType.status_request]
|
|
),
|
|
critical=self.ocsp_must_staple_critical,
|
|
)
|
|
|
|
if self.name_constraints_permitted or self.name_constraints_excluded:
|
|
try:
|
|
csr = csr.add_extension(
|
|
cryptography.x509.NameConstraints(
|
|
[
|
|
cryptography_get_name(
|
|
name, what="name constraints permitted"
|
|
)
|
|
for name in self.name_constraints_permitted
|
|
]
|
|
or None,
|
|
[
|
|
cryptography_get_name(
|
|
name, what="name constraints excluded"
|
|
)
|
|
for name in self.name_constraints_excluded
|
|
]
|
|
or None,
|
|
),
|
|
critical=self.name_constraints_critical,
|
|
)
|
|
except TypeError as e:
|
|
raise OpenSSLObjectError(
|
|
f"Error while parsing name constraint: {e}"
|
|
) from e
|
|
|
|
if self.create_subject_key_identifier:
|
|
if not is_potential_certificate_issuer_public_key(
|
|
self.privatekey.public_key()
|
|
):
|
|
raise OpenSSLObjectError(
|
|
"Private key can not be used to create subject key identifier"
|
|
)
|
|
csr = csr.add_extension(
|
|
cryptography.x509.SubjectKeyIdentifier.from_public_key(
|
|
self.privatekey.public_key()
|
|
),
|
|
critical=False,
|
|
)
|
|
elif self.subject_key_identifier is not None:
|
|
csr = csr.add_extension(
|
|
cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier),
|
|
critical=False,
|
|
)
|
|
|
|
if (
|
|
self.authority_key_identifier is not None
|
|
or self.authority_cert_issuer is not None
|
|
or self.authority_cert_serial_number is not None
|
|
):
|
|
issuers = None
|
|
if self.authority_cert_issuer is not None:
|
|
issuers = [
|
|
cryptography_get_name(n, what="authority cert issuer")
|
|
for n in self.authority_cert_issuer
|
|
]
|
|
csr = csr.add_extension(
|
|
cryptography.x509.AuthorityKeyIdentifier(
|
|
self.authority_key_identifier,
|
|
issuers,
|
|
self.authority_cert_serial_number,
|
|
),
|
|
critical=False,
|
|
)
|
|
|
|
if self.crl_distribution_points:
|
|
csr = csr.add_extension(
|
|
cryptography.x509.CRLDistributionPoints(self.crl_distribution_points),
|
|
critical=False,
|
|
)
|
|
|
|
# csr.sign() does not accept some digests we theoretically could have in digest.
|
|
# For that reason we use type t.Any here. csr.sign() will complain if
|
|
# the digest is not acceptable.
|
|
digest: t.Any | None = None
|
|
if cryptography_key_needs_digest_for_signing(self.privatekey):
|
|
digest = select_message_digest(self.digest)
|
|
if digest is None:
|
|
raise CertificateSigningRequestError(
|
|
f'Unsupported digest "{self.digest}"'
|
|
)
|
|
try:
|
|
self.csr = csr.sign(self.privatekey, digest)
|
|
except UnicodeError as e:
|
|
# This catches IDNAErrors, which happens when a bad name is passed as a SAN
|
|
# (https://github.com/ansible-collections/community.crypto/issues/105).
|
|
# For older cryptography versions, this is handled by idna, which raises
|
|
# an idna.core.IDNAError. Later versions of cryptography deprecated and stopped
|
|
# requiring idna, whence we cannot easily handle this error. Fortunately, in
|
|
# most versions of idna, IDNAError extends UnicodeError. There is only version
|
|
# 2.3 where it extends Exception instead (see
|
|
# https://github.com/kjd/idna/commit/ebefacd3134d0f5da4745878620a6a1cba86d130
|
|
# and then
|
|
# https://github.com/kjd/idna/commit/ea03c7b5db7d2a99af082e0239da2b68aeea702a).
|
|
msg = f"Error while creating CSR: {e}\n"
|
|
if self.using_common_name_for_san:
|
|
self.module.fail_json(
|
|
msg=msg
|
|
+ "This is probably caused because the Common Name is used as a SAN. Specifying use_common_name_for_san=false might fix this."
|
|
)
|
|
self.module.fail_json(
|
|
msg=msg
|
|
+ "This is probably caused by an invalid Subject Alternative DNS Name."
|
|
)
|
|
|
|
def get_csr_data(self) -> bytes:
|
|
"""Return bytes for self.csr."""
|
|
if self.csr is None:
|
|
raise AssertionError("Violated contract: csr is not populated")
|
|
return self.csr.public_bytes(
|
|
cryptography.hazmat.primitives.serialization.Encoding.PEM
|
|
)
|
|
|
|
def _check_csr(self) -> bool:
|
|
"""Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
|
|
if self.existing_csr is None:
|
|
raise AssertionError("Violated contract: existing_csr is not populated")
|
|
if self.privatekey is None:
|
|
raise AssertionError("Violated contract: privatekey is not populated")
|
|
|
|
def _check_subject(csr: cryptography.x509.CertificateSigningRequest) -> bool:
|
|
subject = [
|
|
(cryptography_name_to_oid(entry[0]), to_text(entry[1]))
|
|
for entry in self.subject
|
|
]
|
|
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
|
|
if self.ordered_subject:
|
|
return subject == current_subject
|
|
return set(subject) == set(current_subject)
|
|
|
|
def _find_extension(
|
|
extensions: cryptography.x509.Extensions, exttype: type[_ET]
|
|
) -> cryptography.x509.Extension[_ET] | None:
|
|
return next(
|
|
(ext for ext in extensions if isinstance(ext.value, exttype)), None
|
|
)
|
|
|
|
def _check_subject_alt_name(extensions: cryptography.x509.Extensions) -> bool:
|
|
current_altnames_ext = _find_extension(
|
|
extensions, cryptography.x509.SubjectAlternativeName
|
|
)
|
|
current_altnames = (
|
|
[to_text(altname) for altname in current_altnames_ext.value]
|
|
if current_altnames_ext
|
|
else []
|
|
)
|
|
altnames = (
|
|
[
|
|
to_text(cryptography_get_name(altname))
|
|
for altname in self.subject_alt_name
|
|
]
|
|
if self.subject_alt_name
|
|
else []
|
|
)
|
|
if set(altnames) != set(current_altnames):
|
|
return False
|
|
if altnames and current_altnames_ext:
|
|
if current_altnames_ext.critical != self.subject_alt_name_critical:
|
|
return False
|
|
return True
|
|
|
|
def _check_key_usage(extensions: cryptography.x509.Extensions) -> bool:
|
|
current_keyusage_ext = _find_extension(
|
|
extensions, cryptography.x509.KeyUsage
|
|
)
|
|
if not self.key_usage:
|
|
return current_keyusage_ext is None
|
|
if current_keyusage_ext is None:
|
|
return False
|
|
params = cryptography_parse_key_usage_params(self.key_usage)
|
|
for param, value in params.items():
|
|
if getattr(current_keyusage_ext.value, param) != value:
|
|
return False
|
|
return current_keyusage_ext.critical == self.key_usage_critical
|
|
|
|
def _check_extended_key_usage(extensions: cryptography.x509.Extensions) -> bool:
|
|
current_usages_ext = _find_extension(
|
|
extensions, cryptography.x509.ExtendedKeyUsage
|
|
)
|
|
current_usages = (
|
|
[str(usage) for usage in current_usages_ext.value]
|
|
if current_usages_ext
|
|
else []
|
|
)
|
|
usages = (
|
|
[
|
|
str(cryptography_name_to_oid(usage))
|
|
for usage in self.extended_key_usage
|
|
]
|
|
if self.extended_key_usage
|
|
else []
|
|
)
|
|
if set(current_usages) != set(usages):
|
|
return False
|
|
if usages and current_usages_ext:
|
|
if current_usages_ext.critical != self.extended_key_usage_critical:
|
|
return False
|
|
return True
|
|
|
|
def _check_basic_constraints(extensions: cryptography.x509.Extensions) -> bool:
|
|
bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints)
|
|
current_ca = bc_ext.value.ca if bc_ext else False
|
|
current_path_length = bc_ext.value.path_length if bc_ext else None
|
|
ca, path_length = cryptography_get_basic_constraints(self.basic_constraints)
|
|
# Check CA flag
|
|
if ca != current_ca:
|
|
return False
|
|
# Check path length
|
|
if path_length != current_path_length:
|
|
return False
|
|
# Check criticality
|
|
if self.basic_constraints:
|
|
return (
|
|
bc_ext is not None
|
|
and bc_ext.critical == self.basic_constraints_critical
|
|
)
|
|
return bc_ext is None
|
|
|
|
def _check_ocsp_must_staple(extensions: cryptography.x509.Extensions) -> bool:
|
|
tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature)
|
|
if self.ocsp_must_staple:
|
|
if (
|
|
not tlsfeature_ext
|
|
or tlsfeature_ext.critical != self.ocsp_must_staple_critical
|
|
):
|
|
return False
|
|
return (
|
|
cryptography.x509.TLSFeatureType.status_request
|
|
in tlsfeature_ext.value
|
|
)
|
|
return tlsfeature_ext is None
|
|
|
|
def _check_name_constraints(extensions: cryptography.x509.Extensions) -> bool:
|
|
current_nc_ext = _find_extension(
|
|
extensions, cryptography.x509.NameConstraints
|
|
)
|
|
current_nc_perm = (
|
|
[
|
|
to_text(altname)
|
|
for altname in current_nc_ext.value.permitted_subtrees or []
|
|
]
|
|
if current_nc_ext
|
|
else []
|
|
)
|
|
current_nc_excl = (
|
|
[
|
|
to_text(altname)
|
|
for altname in current_nc_ext.value.excluded_subtrees or []
|
|
]
|
|
if current_nc_ext
|
|
else []
|
|
)
|
|
nc_perm = [
|
|
to_text(
|
|
cryptography_get_name(altname, what="name constraints permitted")
|
|
)
|
|
for altname in self.name_constraints_permitted
|
|
]
|
|
nc_excl = [
|
|
to_text(
|
|
cryptography_get_name(altname, what="name constraints excluded")
|
|
)
|
|
for altname in self.name_constraints_excluded
|
|
]
|
|
if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(
|
|
current_nc_excl
|
|
):
|
|
return False
|
|
if (nc_perm or nc_excl) and current_nc_ext:
|
|
if current_nc_ext.critical != self.name_constraints_critical:
|
|
return False
|
|
return True
|
|
|
|
def _check_subject_key_identifier(
|
|
extensions: cryptography.x509.Extensions,
|
|
) -> bool:
|
|
ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier)
|
|
if (
|
|
self.create_subject_key_identifier
|
|
or self.subject_key_identifier is not None
|
|
):
|
|
if not ext or ext.critical:
|
|
return False
|
|
if self.create_subject_key_identifier:
|
|
assert self.privatekey is not None
|
|
digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(
|
|
self.privatekey.public_key()
|
|
).digest
|
|
return ext.value.digest == digest
|
|
return ext.value.digest == self.subject_key_identifier
|
|
return ext is None
|
|
|
|
def _check_authority_key_identifier(
|
|
extensions: cryptography.x509.Extensions,
|
|
) -> bool:
|
|
ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier)
|
|
if (
|
|
self.authority_key_identifier is not None
|
|
or self.authority_cert_issuer is not None
|
|
or self.authority_cert_serial_number is not None
|
|
):
|
|
if not ext or ext.critical:
|
|
return False
|
|
aci = None
|
|
csr_aci = None
|
|
if self.authority_cert_issuer is not None:
|
|
aci = [
|
|
to_text(cryptography_get_name(n, what="authority cert issuer"))
|
|
for n in self.authority_cert_issuer
|
|
]
|
|
if ext.value.authority_cert_issuer is not None:
|
|
csr_aci = [to_text(n) for n in ext.value.authority_cert_issuer]
|
|
return (
|
|
ext.value.key_identifier == self.authority_key_identifier
|
|
and csr_aci == aci
|
|
and ext.value.authority_cert_serial_number
|
|
== self.authority_cert_serial_number
|
|
)
|
|
return ext is None
|
|
|
|
def _check_crl_distribution_points(
|
|
extensions: cryptography.x509.Extensions,
|
|
) -> bool:
|
|
ext = _find_extension(extensions, cryptography.x509.CRLDistributionPoints)
|
|
if self.crl_distribution_points is None:
|
|
return ext is None
|
|
if not ext:
|
|
return False
|
|
return list(ext.value) == self.crl_distribution_points
|
|
|
|
def _check_extensions(csr: cryptography.x509.CertificateSigningRequest) -> bool:
|
|
extensions = csr.extensions
|
|
return (
|
|
_check_subject_alt_name(extensions)
|
|
and _check_key_usage(extensions)
|
|
and _check_extended_key_usage(extensions)
|
|
and _check_basic_constraints(extensions)
|
|
and _check_ocsp_must_staple(extensions)
|
|
and _check_subject_key_identifier(extensions)
|
|
and _check_authority_key_identifier(extensions)
|
|
and _check_name_constraints(extensions)
|
|
and _check_crl_distribution_points(extensions)
|
|
)
|
|
|
|
def _check_signature(csr: cryptography.x509.CertificateSigningRequest) -> bool:
|
|
if not csr.is_signature_valid:
|
|
return False
|
|
# To check whether public key of CSR belongs to private key,
|
|
# encode both public keys and compare PEMs.
|
|
key_a = csr.public_key().public_bytes(
|
|
cryptography.hazmat.primitives.serialization.Encoding.PEM,
|
|
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
assert self.privatekey is not None
|
|
key_b = self.privatekey.public_key().public_bytes(
|
|
cryptography.hazmat.primitives.serialization.Encoding.PEM,
|
|
cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
return key_a == key_b
|
|
|
|
return (
|
|
_check_subject(self.existing_csr)
|
|
and _check_extensions(self.existing_csr)
|
|
and _check_signature(self.existing_csr)
|
|
)
|
|
|
|
|
|
def select_backend(
|
|
module: AnsibleModule,
|
|
) -> CertificateSigningRequestCryptographyBackend:
|
|
assert_required_cryptography_version(
|
|
module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
|
|
)
|
|
return CertificateSigningRequestCryptographyBackend(module=module)
|
|
|
|
|
|
def get_csr_argument_spec() -> ArgumentSpec:
|
|
return ArgumentSpec(
|
|
argument_spec={
|
|
"digest": {"type": "str", "default": "sha256"},
|
|
"privatekey_path": {"type": "path"},
|
|
"privatekey_content": {"type": "str", "no_log": True},
|
|
"privatekey_passphrase": {"type": "str", "no_log": True},
|
|
"version": {"type": "int", "default": 1, "choices": [1]},
|
|
"subject": {"type": "dict"},
|
|
"subject_ordered": {"type": "list", "elements": "dict"},
|
|
"country_name": {"type": "str", "aliases": ["C", "countryName"]},
|
|
"state_or_province_name": {
|
|
"type": "str",
|
|
"aliases": ["ST", "stateOrProvinceName"],
|
|
},
|
|
"locality_name": {"type": "str", "aliases": ["L", "localityName"]},
|
|
"organization_name": {"type": "str", "aliases": ["O", "organizationName"]},
|
|
"organizational_unit_name": {
|
|
"type": "str",
|
|
"aliases": ["OU", "organizationalUnitName"],
|
|
},
|
|
"common_name": {"type": "str", "aliases": ["CN", "commonName"]},
|
|
"email_address": {"type": "str", "aliases": ["E", "emailAddress"]},
|
|
"subject_alt_name": {
|
|
"type": "list",
|
|
"elements": "str",
|
|
"aliases": ["subjectAltName"],
|
|
},
|
|
"subject_alt_name_critical": {
|
|
"type": "bool",
|
|
"default": False,
|
|
"aliases": ["subjectAltName_critical"],
|
|
},
|
|
"use_common_name_for_san": {
|
|
"type": "bool",
|
|
"default": True,
|
|
"aliases": ["useCommonNameForSAN"],
|
|
},
|
|
"key_usage": {"type": "list", "elements": "str", "aliases": ["keyUsage"]},
|
|
"key_usage_critical": {
|
|
"type": "bool",
|
|
"default": False,
|
|
"aliases": ["keyUsage_critical"],
|
|
},
|
|
"extended_key_usage": {
|
|
"type": "list",
|
|
"elements": "str",
|
|
"aliases": ["extKeyUsage", "extendedKeyUsage"],
|
|
},
|
|
"extended_key_usage_critical": {
|
|
"type": "bool",
|
|
"default": False,
|
|
"aliases": ["extKeyUsage_critical", "extendedKeyUsage_critical"],
|
|
},
|
|
"basic_constraints": {
|
|
"type": "list",
|
|
"elements": "str",
|
|
"aliases": ["basicConstraints"],
|
|
},
|
|
"basic_constraints_critical": {
|
|
"type": "bool",
|
|
"default": False,
|
|
"aliases": ["basicConstraints_critical"],
|
|
},
|
|
"ocsp_must_staple": {
|
|
"type": "bool",
|
|
"default": False,
|
|
"aliases": ["ocspMustStaple"],
|
|
},
|
|
"ocsp_must_staple_critical": {
|
|
"type": "bool",
|
|
"default": False,
|
|
"aliases": ["ocspMustStaple_critical"],
|
|
},
|
|
"name_constraints_permitted": {"type": "list", "elements": "str"},
|
|
"name_constraints_excluded": {"type": "list", "elements": "str"},
|
|
"name_constraints_critical": {"type": "bool", "default": False},
|
|
"create_subject_key_identifier": {"type": "bool", "default": False},
|
|
"subject_key_identifier": {"type": "str"},
|
|
"authority_key_identifier": {"type": "str"},
|
|
"authority_cert_issuer": {"type": "list", "elements": "str"},
|
|
"authority_cert_serial_number": {"type": "int"},
|
|
"crl_distribution_points": {
|
|
"type": "list",
|
|
"elements": "dict",
|
|
"options": {
|
|
"full_name": {"type": "list", "elements": "str"},
|
|
"relative_name": {"type": "list", "elements": "str"},
|
|
"crl_issuer": {"type": "list", "elements": "str"},
|
|
"reasons": {
|
|
"type": "list",
|
|
"elements": "str",
|
|
"choices": [
|
|
"key_compromise",
|
|
"ca_compromise",
|
|
"affiliation_changed",
|
|
"superseded",
|
|
"cessation_of_operation",
|
|
"certificate_hold",
|
|
"privilege_withdrawn",
|
|
"aa_compromise",
|
|
],
|
|
},
|
|
},
|
|
"mutually_exclusive": [("full_name", "relative_name")],
|
|
"required_one_of": [("full_name", "relative_name", "crl_issuer")],
|
|
},
|
|
"select_crypto_backend": {
|
|
"type": "str",
|
|
"default": "auto",
|
|
"choices": ["auto", "cryptography"],
|
|
},
|
|
},
|
|
required_together=[
|
|
["authority_cert_issuer", "authority_cert_serial_number"],
|
|
],
|
|
mutually_exclusive=[
|
|
["privatekey_path", "privatekey_content"],
|
|
["subject", "subject_ordered"],
|
|
],
|
|
required_one_of=[
|
|
["privatekey_path", "privatekey_content"],
|
|
],
|
|
)
|
|
|
|
|
|
__all__ = (
|
|
"CertificateSigningRequestError",
|
|
"CertificateSigningRequestBackend",
|
|
"select_backend",
|
|
"get_csr_argument_spec",
|
|
)
|