mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-03-27 05:43:22 +00:00
* Ensure that *everything* is typed in community.crypto. * Fix comment. * Ignore type definitions/imports and AssertionErrors for code coverage.
243 lines
7.8 KiB
Python
243 lines
7.8 KiB
Python
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
|
# Copyright (c) 2021 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 datetime
|
|
import re
|
|
import typing as t
|
|
|
|
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
|
|
BackendException,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
|
|
OpenSSLObjectError,
|
|
)
|
|
from ansible_collections.community.crypto.plugins.module_utils._time import (
|
|
UTC,
|
|
ensure_utc_timezone,
|
|
from_epoch_seconds,
|
|
get_epoch_seconds,
|
|
get_now_datetime,
|
|
get_relative_time_option,
|
|
remove_timezone,
|
|
)
|
|
|
|
|
|
if t.TYPE_CHECKING:
|
|
import os # pragma: no cover
|
|
|
|
from ansible.module_utils.basic import AnsibleModule # pragma: no cover
|
|
from ansible_collections.community.crypto.plugins.module_utils._acme.certificates import ( # pragma: no cover
|
|
ChainMatcher,
|
|
Criterium,
|
|
)
|
|
|
|
|
|
class CertificateInformation(t.NamedTuple):
|
|
not_valid_after: datetime.datetime
|
|
not_valid_before: datetime.datetime
|
|
serial_number: int
|
|
subject_key_identifier: bytes | None
|
|
authority_key_identifier: bytes | None
|
|
|
|
|
|
_FRACTIONAL_MATCHER = re.compile(
|
|
r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(|\.\d+)(Z|[+-]\d{2}:?\d{2}.*)$"
|
|
)
|
|
|
|
|
|
def _reduce_fractional_digits(timestamp_str: str) -> str:
|
|
"""
|
|
Given a RFC 3339 timestamp that includes too many digits for the fractional seconds part, reduces these to at most 6.
|
|
"""
|
|
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
|
|
m = _FRACTIONAL_MATCHER.match(timestamp_str)
|
|
if not m:
|
|
raise BackendException(f"Cannot parse ISO 8601 timestamp {timestamp_str!r}")
|
|
timestamp, fractional, timezone = m.groups()
|
|
if len(fractional) > 7:
|
|
# Python does not support anything smaller than microseconds
|
|
# (Golang supports nanoseconds, Boulder often emits more fractional digits, which Python chokes on)
|
|
fractional = fractional[:7]
|
|
return f"{timestamp}{fractional}{timezone}"
|
|
|
|
|
|
def _parse_acme_timestamp(
|
|
timestamp_str: str, *, with_timezone: bool
|
|
) -> datetime.datetime:
|
|
"""
|
|
Parses a RFC 3339 timestamp.
|
|
"""
|
|
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
|
|
timestamp_str = _reduce_fractional_digits(timestamp_str)
|
|
for time_format in (
|
|
"%Y-%m-%dT%H:%M:%SZ",
|
|
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
"%Y-%m-%dT%H:%M:%S%z",
|
|
"%Y-%m-%dT%H:%M:%S.%f%z",
|
|
):
|
|
try:
|
|
result = datetime.datetime.strptime(timestamp_str, time_format)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
return (
|
|
ensure_utc_timezone(result)
|
|
if with_timezone
|
|
else remove_timezone(result)
|
|
)
|
|
raise BackendException(f"Cannot parse ISO 8601 timestamp {timestamp_str!r}")
|
|
|
|
|
|
class CryptoBackend(metaclass=abc.ABCMeta):
|
|
def __init__(self, *, module: AnsibleModule, with_timezone: bool = False) -> None:
|
|
self.module = module
|
|
self._with_timezone = with_timezone
|
|
|
|
def get_now(self) -> datetime.datetime:
|
|
return get_now_datetime(with_timezone=self._with_timezone)
|
|
|
|
def parse_acme_timestamp(self, timestamp_str: str) -> datetime.datetime:
|
|
# RFC 3339 (https://www.rfc-editor.org/info/rfc3339)
|
|
return _parse_acme_timestamp(timestamp_str, with_timezone=self._with_timezone)
|
|
|
|
def parse_module_parameter(self, *, value: str, name: str) -> datetime.datetime:
|
|
try:
|
|
result = get_relative_time_option(
|
|
value, input_name=name, with_timezone=self._with_timezone
|
|
)
|
|
if result is None:
|
|
raise BackendException(f"Invalid value for {name}: {value!r}")
|
|
return result
|
|
except OpenSSLObjectError as exc:
|
|
raise BackendException(str(exc)) from exc
|
|
|
|
def interpolate_timestamp(
|
|
self,
|
|
timestamp_start: datetime.datetime,
|
|
timestamp_end: datetime.datetime,
|
|
*,
|
|
percentage: float,
|
|
) -> datetime.datetime:
|
|
start = get_epoch_seconds(timestamp_start)
|
|
end = get_epoch_seconds(timestamp_end)
|
|
return from_epoch_seconds(
|
|
start + percentage * (end - start), with_timezone=self._with_timezone
|
|
)
|
|
|
|
def get_utc_datetime(
|
|
self,
|
|
year: int,
|
|
month: int,
|
|
day: int,
|
|
hour: int = 0,
|
|
minute: int = 0,
|
|
second: int = 0,
|
|
microsecond: int = 0,
|
|
tzinfo: datetime.timezone | None = None,
|
|
) -> datetime.datetime:
|
|
has_tzinfo = tzinfo is not None
|
|
if self._with_timezone and not has_tzinfo:
|
|
tzinfo = UTC
|
|
result = datetime.datetime(
|
|
year, month, day, hour, minute, second, microsecond, tzinfo
|
|
)
|
|
if self._with_timezone and has_tzinfo:
|
|
result = ensure_utc_timezone(result)
|
|
return result
|
|
|
|
@abc.abstractmethod
|
|
def parse_key(
|
|
self,
|
|
*,
|
|
key_file: str | os.PathLike | None = None,
|
|
key_content: str | None = None,
|
|
passphrase: str | None = None,
|
|
) -> dict[str, t.Any]:
|
|
"""
|
|
Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
|
|
Raises KeyParsingError in case of errors.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def sign(
|
|
self, *, payload64: str, protected64: str, key_data: dict[str, t.Any]
|
|
) -> dict[str, t.Any]:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def create_mac_key(self, *, alg: str, key: str) -> dict[str, t.Any]:
|
|
"""Create a MAC key."""
|
|
|
|
@abc.abstractmethod
|
|
def get_ordered_csr_identifiers(
|
|
self,
|
|
*,
|
|
csr_filename: str | os.PathLike | None = None,
|
|
csr_content: str | bytes | None = None,
|
|
) -> list[tuple[str, str]]:
|
|
"""
|
|
Return a list of requested identifiers (CN and SANs) for the CSR.
|
|
Each identifier is a pair (type, identifier), where type is either
|
|
'dns' or 'ip'.
|
|
|
|
The list is deduplicated, and if a CNAME is present, it will be returned
|
|
as the first element in the result.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def get_csr_identifiers(
|
|
self,
|
|
*,
|
|
csr_filename: str | os.PathLike | None = None,
|
|
csr_content: str | bytes | None = None,
|
|
) -> set[tuple[str, str]]:
|
|
"""
|
|
Return a set of requested identifiers (CN and SANs) for the CSR.
|
|
Each identifier is a pair (type, identifier), where type is either
|
|
'dns' or 'ip'.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def get_cert_days(
|
|
self,
|
|
*,
|
|
cert_filename: str | os.PathLike | None = None,
|
|
cert_content: str | bytes | None = None,
|
|
now: datetime.datetime | None = None,
|
|
) -> int:
|
|
"""
|
|
Return the days the certificate in cert_filename remains valid and -1
|
|
if the file was not found. If cert_filename contains more than one
|
|
certificate, only the first one will be considered.
|
|
|
|
If now is not specified, datetime.datetime.now() is used.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def create_chain_matcher(self, *, criterium: Criterium) -> ChainMatcher:
|
|
"""
|
|
Given a Criterium object, creates a ChainMatcher object.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def get_cert_information(
|
|
self,
|
|
*,
|
|
cert_filename: str | os.PathLike | None = None,
|
|
cert_content: str | bytes | None = None,
|
|
) -> CertificateInformation:
|
|
"""
|
|
Return some information on a X.509 certificate as a CertificateInformation object.
|
|
"""
|
|
|
|
|
|
__all__ = ("CertificateInformation", "CryptoBackend")
|