mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
Code refactoring (#889)
* Add __all__ to all module and plugin utils. * Convert quite a few positional args to keyword args. * Avoid Python 3.8+ syntax.
This commit is contained in:
@@ -29,7 +29,7 @@ class ACMEAccount:
|
||||
retrieve account data.
|
||||
"""
|
||||
|
||||
def __init__(self, client: ACMEClient) -> None:
|
||||
def __init__(self, *, client: ACMEClient) -> None:
|
||||
# Set to true to enable logging of all signed requests
|
||||
self._debug: bool = False
|
||||
|
||||
@@ -37,6 +37,7 @@ class ACMEAccount:
|
||||
|
||||
def _new_reg(
|
||||
self,
|
||||
*,
|
||||
contact: list[str] | None = None,
|
||||
terms_agreed: bool = False,
|
||||
allow_creation: bool = True,
|
||||
@@ -82,14 +83,15 @@ class ACMEAccount:
|
||||
url = self.client.directory["newAccount"]
|
||||
if external_account_binding is not None:
|
||||
new_reg["externalAccountBinding"] = self.client.sign_request(
|
||||
{
|
||||
protected={
|
||||
"alg": external_account_binding["alg"],
|
||||
"kid": external_account_binding["kid"],
|
||||
"url": url,
|
||||
},
|
||||
self.client.account_jwk,
|
||||
self.client.backend.create_mac_key(
|
||||
external_account_binding["alg"], external_account_binding["key"]
|
||||
payload=self.client.account_jwk,
|
||||
key_data=self.client.backend.create_mac_key(
|
||||
alg=external_account_binding["alg"],
|
||||
key=external_account_binding["key"],
|
||||
),
|
||||
)
|
||||
elif (
|
||||
@@ -106,7 +108,7 @@ class ACMEAccount:
|
||||
)
|
||||
if not isinstance(result, Mapping):
|
||||
raise ACMEProtocolException(
|
||||
self.client.module,
|
||||
module=self.client.module,
|
||||
msg="Invalid account creation reply from ACME server",
|
||||
info=info,
|
||||
content_json=result,
|
||||
@@ -156,7 +158,7 @@ class ACMEAccount:
|
||||
raise ModuleFailException("Account is deactivated")
|
||||
else:
|
||||
raise ACMEProtocolException(
|
||||
self.client.module,
|
||||
module=self.client.module,
|
||||
msg="Registering ACME account failed",
|
||||
info=info,
|
||||
content_json=result,
|
||||
@@ -187,7 +189,7 @@ class ACMEAccount:
|
||||
)
|
||||
if not isinstance(result, Mapping):
|
||||
raise ACMEProtocolException(
|
||||
self.client.module,
|
||||
module=self.client.module,
|
||||
msg="Invalid account data retrieved from ACME server",
|
||||
info=info,
|
||||
content_json=result,
|
||||
@@ -206,7 +208,7 @@ class ACMEAccount:
|
||||
return None
|
||||
if info["status"] < 200 or info["status"] >= 300:
|
||||
raise ACMEProtocolException(
|
||||
self.client.module,
|
||||
module=self.client.module,
|
||||
msg="Error retrieving account data",
|
||||
info=info,
|
||||
content_json=result,
|
||||
@@ -216,6 +218,7 @@ class ACMEAccount:
|
||||
@t.overload
|
||||
def setup_account(
|
||||
self,
|
||||
*,
|
||||
contact: list[str] | None = None,
|
||||
terms_agreed: bool = False,
|
||||
allow_creation: t.Literal[True] = True,
|
||||
@@ -226,6 +229,7 @@ class ACMEAccount:
|
||||
@t.overload
|
||||
def setup_account(
|
||||
self,
|
||||
*,
|
||||
contact: list[str] | None = None,
|
||||
terms_agreed: bool = False,
|
||||
allow_creation: bool = True,
|
||||
@@ -235,6 +239,7 @@ class ACMEAccount:
|
||||
|
||||
def setup_account(
|
||||
self,
|
||||
*,
|
||||
contact: list[str] | None = None,
|
||||
terms_agreed: bool = False,
|
||||
allow_creation: bool = True,
|
||||
@@ -281,7 +286,7 @@ class ACMEAccount:
|
||||
)
|
||||
else:
|
||||
created, account_data = self._new_reg(
|
||||
contact,
|
||||
contact=contact,
|
||||
terms_agreed=terms_agreed,
|
||||
allow_creation=allow_creation and not self.client.module.check_mode,
|
||||
external_account_binding=external_account_binding,
|
||||
@@ -296,7 +301,7 @@ class ACMEAccount:
|
||||
return created, account_data
|
||||
|
||||
def update_account(
|
||||
self, account_data: dict[str, t.Any], contact: list[str] | None = None
|
||||
self, *, account_data: dict[str, t.Any], contact: list[str] | None = None
|
||||
) -> tuple[bool, dict[str, t.Any]]:
|
||||
"""
|
||||
Update an account on the ACME server. Check mode is fully respected.
|
||||
@@ -332,10 +337,13 @@ class ACMEAccount:
|
||||
)
|
||||
if not isinstance(account_data, Mapping):
|
||||
raise ACMEProtocolException(
|
||||
self.client.module,
|
||||
module=self.client.module,
|
||||
msg="Invalid account updating reply from ACME server",
|
||||
info=info,
|
||||
content_json=account_data,
|
||||
)
|
||||
|
||||
return True, account_data
|
||||
|
||||
|
||||
__all__ = ("ACMEAccount",)
|
||||
|
||||
@@ -65,14 +65,14 @@ RETRY_COUNT = 10
|
||||
|
||||
|
||||
def _decode_retry(
|
||||
module: AnsibleModule, response: t.Any, info: dict[str, t.Any], retry_count: int
|
||||
*, module: AnsibleModule, response: t.Any, info: dict[str, t.Any], retry_count: int
|
||||
) -> bool:
|
||||
if info["status"] not in RETRY_STATUS_CODES:
|
||||
return False
|
||||
|
||||
if retry_count >= RETRY_COUNT:
|
||||
raise ACMEProtocolException(
|
||||
module,
|
||||
module=module,
|
||||
msg=f"Giving up after {RETRY_COUNT} retries",
|
||||
info=info,
|
||||
response=response,
|
||||
@@ -93,6 +93,7 @@ def _decode_retry(
|
||||
|
||||
|
||||
def _assert_fetch_url_success(
|
||||
*,
|
||||
module: AnsibleModule,
|
||||
response: t.Any,
|
||||
info: dict[str, t.Any],
|
||||
@@ -108,11 +109,11 @@ def _assert_fetch_url_success(
|
||||
or (400 <= info["status"] < 500 and not allow_client_error)
|
||||
or (info["status"] >= 500 and not allow_server_error)
|
||||
):
|
||||
raise ACMEProtocolException(module, info=info, response=response)
|
||||
raise ACMEProtocolException(module=module, info=info, response=response)
|
||||
|
||||
|
||||
def _is_failed(
|
||||
info: dict[str, t.Any], expected_status_codes: t.Iterable[int] | None = None
|
||||
*, info: dict[str, t.Any], expected_status_codes: t.Iterable[int] | None = None
|
||||
) -> bool:
|
||||
if info["status"] < 200 or info["status"] >= 400:
|
||||
return True
|
||||
@@ -133,7 +134,7 @@ class ACMEDirectory:
|
||||
https://tools.ietf.org/html/rfc8555#section-7.1.1
|
||||
"""
|
||||
|
||||
def __init__(self, module: AnsibleModule, client: ACMEClient) -> None:
|
||||
def __init__(self, *, module: AnsibleModule, client: ACMEClient) -> None:
|
||||
self.module = module
|
||||
self.directory_root = module.params["acme_directory"]
|
||||
self.version = module.params["acme_version"]
|
||||
@@ -171,7 +172,12 @@ class ACMEDirectory:
|
||||
response, info = fetch_url(
|
||||
self.module, url, method="HEAD", timeout=self.request_timeout
|
||||
)
|
||||
if _decode_retry(self.module, response, info, retry_count):
|
||||
if _decode_retry(
|
||||
module=self.module,
|
||||
response=response,
|
||||
info=info,
|
||||
retry_count=retry_count,
|
||||
):
|
||||
retry_count += 1
|
||||
continue
|
||||
if info["status"] not in (200, 204):
|
||||
@@ -185,7 +191,7 @@ class ACMEDirectory:
|
||||
)
|
||||
if retry_count >= 5:
|
||||
raise ACMEProtocolException(
|
||||
self.module,
|
||||
module=self.module,
|
||||
msg="Was not able to obtain nonce, giving up after 5 retries",
|
||||
info=info,
|
||||
response=response,
|
||||
@@ -202,7 +208,7 @@ class ACMEClient:
|
||||
ACME server.
|
||||
"""
|
||||
|
||||
def __init__(self, module: AnsibleModule, backend: CryptoBackend) -> None:
|
||||
def __init__(self, *, module: AnsibleModule, backend: CryptoBackend) -> None:
|
||||
# Set to true to enable logging of all signed requests
|
||||
self._debug = False
|
||||
|
||||
@@ -241,7 +247,7 @@ class ACMEClient:
|
||||
# Make sure self.account_jws_header is updated
|
||||
self.set_account_uri(self.account_uri)
|
||||
|
||||
self.directory = ACMEDirectory(module, self)
|
||||
self.directory = ACMEDirectory(module=module, client=self)
|
||||
|
||||
def set_account_uri(self, uri: str) -> None:
|
||||
"""
|
||||
@@ -255,6 +261,7 @@ class ACMEClient:
|
||||
|
||||
def parse_key(
|
||||
self,
|
||||
*,
|
||||
key_file: str | os.PathLike | None = None,
|
||||
key_content: str | None = None,
|
||||
passphrase: str | None = None,
|
||||
@@ -265,10 +272,13 @@ class ACMEClient:
|
||||
"""
|
||||
if key_file is None and key_content is None:
|
||||
raise AssertionError("One of key_file and key_content must be specified!")
|
||||
return self.backend.parse_key(key_file, key_content, passphrase=passphrase)
|
||||
return self.backend.parse_key(
|
||||
key_file=key_file, key_content=key_content, passphrase=passphrase
|
||||
)
|
||||
|
||||
def sign_request(
|
||||
self,
|
||||
*,
|
||||
protected: dict[str, t.Any],
|
||||
payload: str | dict[str, t.Any] | None,
|
||||
key_data: dict[str, t.Any],
|
||||
@@ -292,9 +302,11 @@ class ACMEClient:
|
||||
f"Failed to encode payload / headers as JSON: {e}"
|
||||
)
|
||||
|
||||
return self.backend.sign(payload64, protected64, key_data)
|
||||
return self.backend.sign(
|
||||
payload64=payload64, protected64=protected64, key_data=key_data
|
||||
)
|
||||
|
||||
def _log(self, msg: str, data: t.Any = None) -> None:
|
||||
def _log(self, msg: str, *, data: t.Any = None) -> None:
|
||||
"""
|
||||
Write arguments to acme.log when logging is enabled.
|
||||
"""
|
||||
@@ -373,13 +385,16 @@ class ACMEClient:
|
||||
protected["nonce"] = self.directory.get_nonce()
|
||||
protected["url"] = url
|
||||
|
||||
self._log("URL", url)
|
||||
self._log("protected", protected)
|
||||
self._log("payload", payload)
|
||||
self._log("URL", data=url)
|
||||
self._log("protected", data=protected)
|
||||
self._log("payload", data=payload)
|
||||
data = self.sign_request(
|
||||
protected, payload, key_data, encode_payload=encode_payload
|
||||
protected=protected,
|
||||
payload=payload,
|
||||
key_data=key_data,
|
||||
encode_payload=encode_payload,
|
||||
)
|
||||
self._log("signed request", data)
|
||||
self._log("signed request", data=data)
|
||||
data = self.module.jsonify(data)
|
||||
|
||||
headers = {
|
||||
@@ -393,10 +408,12 @@ class ACMEClient:
|
||||
method="POST",
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if _decode_retry(self.module, resp, info, failed_tries):
|
||||
if _decode_retry(
|
||||
module=self.module, response=resp, info=info, retry_count=failed_tries
|
||||
):
|
||||
failed_tries += 1
|
||||
continue
|
||||
_assert_fetch_url_success(self.module, resp, info)
|
||||
_assert_fetch_url_success(module=self.module, response=resp, info=info)
|
||||
result = {}
|
||||
|
||||
try:
|
||||
@@ -415,7 +432,7 @@ class ACMEClient:
|
||||
) or 400 <= info["status"] < 600:
|
||||
try:
|
||||
decoded_result = self.module.from_json(content.decode("utf8"))
|
||||
self._log("parsed result", decoded_result)
|
||||
self._log("parsed result", data=decoded_result)
|
||||
# In case of badNonce error, try again (up to 5 times)
|
||||
# (https://tools.ietf.org/html/rfc8555#section-6.7)
|
||||
if all(
|
||||
@@ -440,10 +457,10 @@ class ACMEClient:
|
||||
result = content
|
||||
|
||||
if fail_on_error and _is_failed(
|
||||
info, expected_status_codes=expected_status_codes
|
||||
info=info, expected_status_codes=expected_status_codes
|
||||
):
|
||||
raise ACMEProtocolException(
|
||||
self.module,
|
||||
module=self.module,
|
||||
msg=error_msg,
|
||||
info=info,
|
||||
content=content,
|
||||
@@ -515,11 +532,16 @@ class ACMEClient:
|
||||
headers=headers,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if not _decode_retry(self.module, resp, info, retry_count):
|
||||
if not _decode_retry(
|
||||
module=self.module,
|
||||
response=resp,
|
||||
info=info,
|
||||
retry_count=retry_count,
|
||||
):
|
||||
break
|
||||
retry_count += 1
|
||||
|
||||
_assert_fetch_url_success(self.module, resp, info)
|
||||
_assert_fetch_url_success(module=self.module, response=resp, info=info)
|
||||
|
||||
try:
|
||||
# In Python 2, reading from a closed response yields a TypeError.
|
||||
@@ -550,10 +572,10 @@ class ACMEClient:
|
||||
result = content
|
||||
|
||||
if fail_on_error and _is_failed(
|
||||
info, expected_status_codes=expected_status_codes
|
||||
info=info, expected_status_codes=expected_status_codes
|
||||
):
|
||||
raise ACMEProtocolException(
|
||||
self.module,
|
||||
module=self.module,
|
||||
msg=error_msg,
|
||||
info=info,
|
||||
content=content,
|
||||
@@ -565,6 +587,7 @@ class ACMEClient:
|
||||
|
||||
def get_renewal_info(
|
||||
self,
|
||||
*,
|
||||
cert_id: str | None = None,
|
||||
cert_info: CertificateInformation | None = None,
|
||||
cert_filename: str | os.PathLike | None = None,
|
||||
@@ -579,7 +602,7 @@ class ACMEClient:
|
||||
|
||||
if cert_id is None:
|
||||
cert_id = compute_cert_id(
|
||||
self.backend,
|
||||
backend=self.backend,
|
||||
cert_info=cert_info,
|
||||
cert_filename=cert_filename,
|
||||
cert_content=cert_content,
|
||||
@@ -603,6 +626,7 @@ class ACMEClient:
|
||||
|
||||
|
||||
def create_default_argspec(
|
||||
*,
|
||||
with_account: bool = True,
|
||||
require_account_key: bool = True,
|
||||
with_certificate: bool = False,
|
||||
@@ -643,7 +667,9 @@ def create_default_argspec(
|
||||
return result
|
||||
|
||||
|
||||
def create_backend(module: AnsibleModule, needs_acme_v2: bool = True) -> CryptoBackend:
|
||||
def create_backend(
|
||||
module: AnsibleModule, *, needs_acme_v2: bool = True
|
||||
) -> CryptoBackend:
|
||||
backend = module.params["select_crypto_backend"]
|
||||
|
||||
# Backend autodetect
|
||||
@@ -671,10 +697,10 @@ def create_backend(module: AnsibleModule, needs_acme_v2: bool = True) -> CryptoB
|
||||
module.debug(
|
||||
f"Using cryptography backend (library version {CRYPTOGRAPHY_VERSION})"
|
||||
)
|
||||
module_backend = CryptographyBackend(module)
|
||||
module_backend = CryptographyBackend(module=module)
|
||||
elif backend == "openssl":
|
||||
module.debug("Using OpenSSL binary backend")
|
||||
module_backend = OpenSSLCLIBackend(module)
|
||||
module_backend = OpenSSLCLIBackend(module=module)
|
||||
else:
|
||||
module.fail_json(msg=f'Unknown crypto backend "{backend}"!')
|
||||
|
||||
@@ -691,3 +717,11 @@ def create_backend(module: AnsibleModule, needs_acme_v2: bool = True) -> CryptoB
|
||||
locale.setlocale(locale.LC_ALL, "C")
|
||||
|
||||
return module_backend
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ACMEDirectory",
|
||||
"ACMEClient",
|
||||
"create_default_argspec",
|
||||
"create_backend",
|
||||
)
|
||||
|
||||
@@ -92,7 +92,11 @@ if t.TYPE_CHECKING:
|
||||
class CryptographyChainMatcher(ChainMatcher):
|
||||
@staticmethod
|
||||
def _parse_key_identifier(
|
||||
key_identifier: str | None, name: str, criterium_idx: int, module: AnsibleModule
|
||||
*,
|
||||
key_identifier: str | None,
|
||||
name: str,
|
||||
criterium_idx: int,
|
||||
module: AnsibleModule,
|
||||
) -> bytes | None:
|
||||
if key_identifier:
|
||||
try:
|
||||
@@ -109,7 +113,7 @@ class CryptographyChainMatcher(ChainMatcher):
|
||||
)
|
||||
return None
|
||||
|
||||
def __init__(self, criterium: Criterium, module: AnsibleModule) -> None:
|
||||
def __init__(self, *, criterium: Criterium, module: AnsibleModule) -> None:
|
||||
self.criterium = criterium
|
||||
self.test_certificates = criterium.test_certificates
|
||||
self.subject: list[tuple[cryptography.x509.oid.ObjectIdentifier, str]] = []
|
||||
@@ -117,29 +121,32 @@ class CryptographyChainMatcher(ChainMatcher):
|
||||
if criterium.subject:
|
||||
self.subject = [
|
||||
(cryptography_name_to_oid(k), to_native(v))
|
||||
for k, v in parse_name_field(criterium.subject, "subject")
|
||||
for k, v in parse_name_field(
|
||||
criterium.subject, name_field_name="subject"
|
||||
)
|
||||
]
|
||||
if criterium.issuer:
|
||||
self.issuer = [
|
||||
(cryptography_name_to_oid(k), to_native(v))
|
||||
for k, v in parse_name_field(criterium.issuer, "issuer")
|
||||
for k, v in parse_name_field(criterium.issuer, name_field_name="issuer")
|
||||
]
|
||||
self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
|
||||
criterium.subject_key_identifier,
|
||||
"subject_key_identifier",
|
||||
criterium.index,
|
||||
module,
|
||||
key_identifier=criterium.subject_key_identifier,
|
||||
name="subject_key_identifier",
|
||||
criterium_idx=criterium.index,
|
||||
module=module,
|
||||
)
|
||||
self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier(
|
||||
criterium.authority_key_identifier,
|
||||
"authority_key_identifier",
|
||||
criterium.index,
|
||||
module,
|
||||
key_identifier=criterium.authority_key_identifier,
|
||||
name="authority_key_identifier",
|
||||
criterium_idx=criterium.index,
|
||||
module=module,
|
||||
)
|
||||
self.module = module
|
||||
|
||||
def _match_subject(
|
||||
self,
|
||||
*,
|
||||
x509_subject: cryptography.x509.Name,
|
||||
match_subject: list[tuple[cryptography.x509.oid.ObjectIdentifier, str]],
|
||||
) -> bool:
|
||||
@@ -153,7 +160,7 @@ class CryptographyChainMatcher(ChainMatcher):
|
||||
return False
|
||||
return True
|
||||
|
||||
def match(self, certificate: CertificateChain) -> bool:
|
||||
def match(self, *, certificate: CertificateChain) -> bool:
|
||||
"""
|
||||
Check whether an alternate chain matches the specified criterium.
|
||||
"""
|
||||
@@ -166,9 +173,13 @@ class CryptographyChainMatcher(ChainMatcher):
|
||||
try:
|
||||
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert))
|
||||
matches = True
|
||||
if not self._match_subject(x509.subject, self.subject):
|
||||
if not self._match_subject(
|
||||
x509_subject=x509.subject, match_subject=self.subject
|
||||
):
|
||||
matches = False
|
||||
if not self._match_subject(x509.issuer, self.issuer):
|
||||
if not self._match_subject(
|
||||
x509_subject=x509.issuer, match_subject=self.issuer
|
||||
):
|
||||
matches = False
|
||||
if self.subject_key_identifier:
|
||||
try:
|
||||
@@ -199,13 +210,14 @@ class CryptographyChainMatcher(ChainMatcher):
|
||||
|
||||
|
||||
class CryptographyBackend(CryptoBackend):
|
||||
def __init__(self, module: AnsibleModule) -> None:
|
||||
def __init__(self, *, module: AnsibleModule) -> None:
|
||||
super(CryptographyBackend, self).__init__(
|
||||
module, with_timezone=CRYPTOGRAPHY_TIMEZONE
|
||||
module=module, with_timezone=CRYPTOGRAPHY_TIMEZONE
|
||||
)
|
||||
|
||||
def parse_key(
|
||||
self,
|
||||
*,
|
||||
key_file: str | os.PathLike | None = None,
|
||||
key_content: str | None = None,
|
||||
passphrase: str | None = None,
|
||||
@@ -288,7 +300,7 @@ class CryptographyBackend(CryptoBackend):
|
||||
raise KeyParsingError(f'unknown key type "{type(key)}"')
|
||||
|
||||
def sign(
|
||||
self, payload64: str, protected64: str, key_data: dict[str, t.Any]
|
||||
self, *, payload64: str, protected64: str, key_data: dict[str, t.Any]
|
||||
) -> dict[str, t.Any]:
|
||||
sign_payload = f"{protected64}.{payload64}".encode("utf8")
|
||||
hashalg: type[cryptography.hazmat.primitives.hashes.HashAlgorithm]
|
||||
@@ -317,8 +329,8 @@ class CryptographyBackend(CryptoBackend):
|
||||
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(
|
||||
key_data["key_obj"].sign(sign_payload, ecdsa)
|
||||
)
|
||||
rr = convert_int_to_hex(r, 2 * key_data["point_size"])
|
||||
ss = convert_int_to_hex(s, 2 * key_data["point_size"])
|
||||
rr = convert_int_to_hex(r, digits=2 * key_data["point_size"])
|
||||
ss = convert_int_to_hex(s, digits=2 * key_data["point_size"])
|
||||
signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
|
||||
|
||||
return {
|
||||
@@ -327,7 +339,7 @@ class CryptographyBackend(CryptoBackend):
|
||||
"signature": nopad_b64(signature),
|
||||
}
|
||||
|
||||
def create_mac_key(self, alg: str, key: str) -> dict[str, t.Any]:
|
||||
def create_mac_key(self, *, alg: str, key: str) -> dict[str, t.Any]:
|
||||
"""Create a MAC key."""
|
||||
hashalg: type[cryptography.hazmat.primitives.hashes.HashAlgorithm]
|
||||
if alg == "HS256":
|
||||
@@ -362,6 +374,7 @@ class CryptographyBackend(CryptoBackend):
|
||||
|
||||
def get_ordered_csr_identifiers(
|
||||
self,
|
||||
*,
|
||||
csr_filename: str | os.PathLike | None = None,
|
||||
csr_content: str | bytes | None = None,
|
||||
) -> list[tuple[str, str]]:
|
||||
@@ -413,6 +426,7 @@ class CryptographyBackend(CryptoBackend):
|
||||
|
||||
def get_csr_identifiers(
|
||||
self,
|
||||
*,
|
||||
csr_filename: str | os.PathLike | None = None,
|
||||
csr_content: str | bytes | bytes | None = None,
|
||||
) -> set[tuple[str, str]]:
|
||||
@@ -429,6 +443,7 @@ class CryptographyBackend(CryptoBackend):
|
||||
|
||||
def get_cert_days(
|
||||
self,
|
||||
*,
|
||||
cert_filename: str | os.PathLike | None = None,
|
||||
cert_content: str | bytes | None = None,
|
||||
now: datetime.datetime | None = None,
|
||||
@@ -466,14 +481,15 @@ class CryptographyBackend(CryptoBackend):
|
||||
now = add_or_remove_timezone(now, with_timezone=CRYPTOGRAPHY_TIMEZONE)
|
||||
return (get_not_valid_after(cert) - now).days
|
||||
|
||||
def create_chain_matcher(self, criterium: Criterium) -> ChainMatcher:
|
||||
def create_chain_matcher(self, *, criterium: Criterium) -> ChainMatcher:
|
||||
"""
|
||||
Given a Criterium object, creates a ChainMatcher object.
|
||||
"""
|
||||
return CryptographyChainMatcher(criterium, self.module)
|
||||
return CryptographyChainMatcher(criterium=criterium, module=self.module)
|
||||
|
||||
def get_cert_information(
|
||||
self,
|
||||
*,
|
||||
cert_filename: str | os.PathLike | None = None,
|
||||
cert_content: str | bytes | None = None,
|
||||
) -> CertificateInformation:
|
||||
@@ -520,3 +536,12 @@ class CryptographyBackend(CryptoBackend):
|
||||
subject_key_identifier=ski,
|
||||
authority_key_identifier=aki,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"CRYPTOGRAPHY_MINIMAL_VERSION",
|
||||
"CRYPTOGRAPHY_ERROR",
|
||||
"CRYPTOGRAPHY_VERSION",
|
||||
"CRYPTOGRAPHY_ERROR",
|
||||
"CryptographyBackend",
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ _OPENSSL_ENVIRONMENT_UPDATE = dict(LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTY
|
||||
|
||||
|
||||
def _extract_date(
|
||||
out_text: str, name: str, cert_filename_suffix: str = ""
|
||||
out_text: str, *, name: str, cert_filename_suffix: str = ""
|
||||
) -> datetime.datetime:
|
||||
matcher = re.search(rf"\s+{name}\s*:\s+(.*)", out_text)
|
||||
if matcher is None:
|
||||
@@ -76,6 +76,7 @@ def _decode_octets(octets_text: str) -> bytes:
|
||||
@t.overload
|
||||
def _extract_octets(
|
||||
out_text: str,
|
||||
*,
|
||||
name: str,
|
||||
required: t.Literal[False],
|
||||
potential_prefixes: t.Iterable[str] | None = None,
|
||||
@@ -85,6 +86,7 @@ def _extract_octets(
|
||||
@t.overload
|
||||
def _extract_octets(
|
||||
out_text: str,
|
||||
*,
|
||||
name: str,
|
||||
required: t.Literal[True],
|
||||
potential_prefixes: t.Iterable[str] | None = None,
|
||||
@@ -93,6 +95,7 @@ def _extract_octets(
|
||||
|
||||
def _extract_octets(
|
||||
out_text: str,
|
||||
*,
|
||||
name: str,
|
||||
required: bool = True,
|
||||
potential_prefixes: t.Iterable[str] | None = None,
|
||||
@@ -113,15 +116,16 @@ def _extract_octets(
|
||||
|
||||
class OpenSSLCLIBackend(CryptoBackend):
|
||||
def __init__(
|
||||
self, module: AnsibleModule, openssl_binary: str | None = None
|
||||
self, *, module: AnsibleModule, openssl_binary: str | None = None
|
||||
) -> None:
|
||||
super(OpenSSLCLIBackend, self).__init__(module, with_timezone=True)
|
||||
super(OpenSSLCLIBackend, self).__init__(module=module, with_timezone=True)
|
||||
if openssl_binary is None:
|
||||
openssl_binary = module.get_bin_path("openssl", True)
|
||||
self.openssl_binary = openssl_binary
|
||||
|
||||
def parse_key(
|
||||
self,
|
||||
*,
|
||||
key_file: str | os.PathLike | None = None,
|
||||
key_content: str | None = None,
|
||||
passphrase: str | None = None,
|
||||
@@ -282,7 +286,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
)
|
||||
|
||||
def sign(
|
||||
self, payload64: str, protected64: str, key_data: dict[str, t.Any]
|
||||
self, *, payload64: str, protected64: str, key_data: dict[str, t.Any]
|
||||
) -> dict[str, t.Any]:
|
||||
sign_payload = f"{protected64}.{payload64}".encode("utf8")
|
||||
if key_data["type"] == "hmac":
|
||||
@@ -343,7 +347,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
"signature": nopad_b64(to_bytes(out)),
|
||||
}
|
||||
|
||||
def create_mac_key(self, alg: str, key: str) -> dict[str, t.Any]:
|
||||
def create_mac_key(self, *, alg: str, key: str) -> dict[str, t.Any]:
|
||||
"""Create a MAC key."""
|
||||
if alg == "HS256":
|
||||
hashalg = "sha256"
|
||||
@@ -383,6 +387,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
|
||||
def get_ordered_csr_identifiers(
|
||||
self,
|
||||
*,
|
||||
csr_filename: str | os.PathLike | None = None,
|
||||
csr_content: str | bytes | None = None,
|
||||
) -> list[tuple[str, str]]:
|
||||
@@ -454,6 +459,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
|
||||
def get_csr_identifiers(
|
||||
self,
|
||||
*,
|
||||
csr_filename: str | os.PathLike | None = None,
|
||||
csr_content: str | bytes | None = None,
|
||||
) -> set[tuple[str, str]]:
|
||||
@@ -470,6 +476,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
|
||||
def get_cert_days(
|
||||
self,
|
||||
*,
|
||||
cert_filename: str | os.PathLike | None = None,
|
||||
cert_content: str | bytes | None = None,
|
||||
now: datetime.datetime | None = None,
|
||||
@@ -516,7 +523,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
|
||||
out_text = to_text(out, errors="surrogate_or_strict")
|
||||
not_after = _extract_date(
|
||||
out_text, "Not After", cert_filename_suffix=cert_filename_suffix
|
||||
out_text, name="Not After", cert_filename_suffix=cert_filename_suffix
|
||||
)
|
||||
if now is None:
|
||||
now = self.get_now()
|
||||
@@ -524,7 +531,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
now = ensure_utc_timezone(now)
|
||||
return (not_after - now).days
|
||||
|
||||
def create_chain_matcher(self, criterium: Criterium) -> t.NoReturn:
|
||||
def create_chain_matcher(self, *, criterium: Criterium) -> t.NoReturn:
|
||||
"""
|
||||
Given a Criterium object, creates a ChainMatcher object.
|
||||
"""
|
||||
@@ -534,6 +541,7 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
|
||||
def get_cert_information(
|
||||
self,
|
||||
*,
|
||||
cert_filename: str | os.PathLike | None = None,
|
||||
cert_content: str | bytes | None = None,
|
||||
) -> CertificateInformation:
|
||||
@@ -572,10 +580,10 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
out_text = to_text(out, errors="surrogate_or_strict")
|
||||
|
||||
not_after = _extract_date(
|
||||
out_text, "Not After", cert_filename_suffix=cert_filename_suffix
|
||||
out_text, name="Not After", cert_filename_suffix=cert_filename_suffix
|
||||
)
|
||||
not_before = _extract_date(
|
||||
out_text, "Not Before", cert_filename_suffix=cert_filename_suffix
|
||||
out_text, name="Not Before", cert_filename_suffix=cert_filename_suffix
|
||||
)
|
||||
|
||||
sn = re.search(
|
||||
@@ -587,13 +595,15 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
serial = int(sn.group(1))
|
||||
else:
|
||||
serial = convert_bytes_to_int(
|
||||
_extract_octets(out_text, "Serial Number", required=True)
|
||||
_extract_octets(out_text, name="Serial Number", required=True)
|
||||
)
|
||||
|
||||
ski = _extract_octets(out_text, "X509v3 Subject Key Identifier", required=False)
|
||||
ski = _extract_octets(
|
||||
out_text, name="X509v3 Subject Key Identifier", required=False
|
||||
)
|
||||
aki = _extract_octets(
|
||||
out_text,
|
||||
"X509v3 Authority Key Identifier",
|
||||
name="X509v3 Authority Key Identifier",
|
||||
required=False,
|
||||
potential_prefixes=["keyid:", ""],
|
||||
)
|
||||
@@ -605,3 +615,6 @@ class OpenSSLCLIBackend(CryptoBackend):
|
||||
subject_key_identifier=ski,
|
||||
authority_key_identifier=aki,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ("OpenSSLCLIBackend",)
|
||||
|
||||
@@ -69,7 +69,9 @@ def _reduce_fractional_digits(timestamp_str: str) -> str:
|
||||
return f"{timestamp}{fractional}{timezone}"
|
||||
|
||||
|
||||
def _parse_acme_timestamp(timestamp_str: str, with_timezone: bool) -> datetime.datetime:
|
||||
def _parse_acme_timestamp(
|
||||
timestamp_str: str, *, with_timezone: bool
|
||||
) -> datetime.datetime:
|
||||
"""
|
||||
Parses a RFC 3339 timestamp.
|
||||
"""
|
||||
@@ -95,7 +97,7 @@ def _parse_acme_timestamp(timestamp_str: str, with_timezone: bool) -> datetime.d
|
||||
|
||||
|
||||
class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
def __init__(self, module: AnsibleModule, with_timezone: bool = False) -> None:
|
||||
def __init__(self, *, module: AnsibleModule, with_timezone: bool = False) -> None:
|
||||
self.module = module
|
||||
self._with_timezone = with_timezone
|
||||
|
||||
@@ -106,10 +108,10 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
# 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:
|
||||
def parse_module_parameter(self, *, value: str, name: str) -> datetime.datetime:
|
||||
try:
|
||||
result = get_relative_time_option(
|
||||
value, name, with_timezone=self._with_timezone
|
||||
value, input_name=name, with_timezone=self._with_timezone
|
||||
)
|
||||
if result is None:
|
||||
raise BackendException(f"Invalid value for {name}: {value!r}")
|
||||
@@ -121,6 +123,7 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
self,
|
||||
timestamp_start: datetime.datetime,
|
||||
timestamp_end: datetime.datetime,
|
||||
*,
|
||||
percentage: float,
|
||||
) -> datetime.datetime:
|
||||
start = get_epoch_seconds(timestamp_start)
|
||||
@@ -141,6 +144,7 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def parse_key(
|
||||
self,
|
||||
*,
|
||||
key_file: str | os.PathLike | None = None,
|
||||
key_content: str | None = None,
|
||||
passphrase: str | None = None,
|
||||
@@ -152,17 +156,18 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def sign(
|
||||
self, payload64: str, protected64: str, key_data: dict[str, t.Any]
|
||||
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]:
|
||||
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]]:
|
||||
@@ -178,6 +183,7 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def get_csr_identifiers(
|
||||
self,
|
||||
*,
|
||||
csr_filename: str | os.PathLike | None = None,
|
||||
csr_content: str | bytes | None = None,
|
||||
) -> set[tuple[str, str]]:
|
||||
@@ -190,6 +196,7 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
@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,
|
||||
@@ -203,7 +210,7 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def create_chain_matcher(self, criterium: Criterium) -> ChainMatcher:
|
||||
def create_chain_matcher(self, *, criterium: Criterium) -> ChainMatcher:
|
||||
"""
|
||||
Given a Criterium object, creates a ChainMatcher object.
|
||||
"""
|
||||
@@ -211,9 +218,13 @@ class CryptoBackend(metaclass=abc.ABCMeta):
|
||||
@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")
|
||||
|
||||
@@ -58,6 +58,7 @@ class ACMECertificateClient:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
module: AnsibleModule,
|
||||
backend: CryptoBackend,
|
||||
client: ACMEClient | None = None,
|
||||
@@ -68,10 +69,10 @@ class ACMECertificateClient:
|
||||
self.csr = module.params.get("csr")
|
||||
self.csr_content = module.params.get("csr_content")
|
||||
if client is None:
|
||||
client = ACMEClient(module, backend)
|
||||
client = ACMEClient(module=module, backend=backend)
|
||||
self.client = client
|
||||
if account is None:
|
||||
account = ACMEAccount(self.client)
|
||||
account = ACMEAccount(client=self.client)
|
||||
self.account = account
|
||||
self.order_uri = module.params.get("order_uri")
|
||||
self.order_creation_error_strategy = module.params.get(
|
||||
@@ -108,7 +109,9 @@ class ACMECertificateClient:
|
||||
try:
|
||||
select_chain_matcher.append(
|
||||
self.client.backend.create_chain_matcher(
|
||||
Criterium(criterium, index=criterium_idx)
|
||||
criterium=Criterium(
|
||||
criterium=criterium, index=criterium_idx
|
||||
)
|
||||
)
|
||||
)
|
||||
except ValueError as exc:
|
||||
@@ -120,12 +123,12 @@ class ACMECertificateClient:
|
||||
def load_order(self) -> Order:
|
||||
if not self.order_uri:
|
||||
raise ModuleFailException("The order URI has not been provided")
|
||||
order = Order.from_url(self.client, self.order_uri)
|
||||
order.load_authorizations(self.client)
|
||||
order = Order.from_url(client=self.client, url=self.order_uri)
|
||||
order.load_authorizations(client=self.client)
|
||||
return order
|
||||
|
||||
def create_order(
|
||||
self, replaces_cert_id: str | None = None, profile: str | None = None
|
||||
self, *, replaces_cert_id: str | None = None, profile: str | None = None
|
||||
) -> Order:
|
||||
"""
|
||||
Create a new order.
|
||||
@@ -133,8 +136,8 @@ class ACMECertificateClient:
|
||||
if self.identifiers is None:
|
||||
raise ModuleFailException("No identifiers have been provided")
|
||||
order = Order.create_with_error_handling(
|
||||
self.client,
|
||||
self.identifiers,
|
||||
client=self.client,
|
||||
identifiers=self.identifiers,
|
||||
error_strategy=self.order_creation_error_strategy,
|
||||
error_max_retries=self.order_creation_max_retries,
|
||||
replaces_cert_id=replaces_cert_id,
|
||||
@@ -142,7 +145,7 @@ class ACMECertificateClient:
|
||||
message_callback=self.module.warn,
|
||||
)
|
||||
self.order_uri = order.url
|
||||
order.load_authorizations(self.client)
|
||||
order.load_authorizations(client=self.client)
|
||||
return order
|
||||
|
||||
def get_challenges_data(
|
||||
@@ -161,7 +164,7 @@ class ACMECertificateClient:
|
||||
# and do not need to be returned
|
||||
if authz.status == "valid":
|
||||
continue
|
||||
challenge_data = authz.get_challenge_data(self.client)
|
||||
challenge_data = authz.get_challenge_data(client=self.client)
|
||||
data.append(
|
||||
dict(
|
||||
identifier=authz.identifier,
|
||||
@@ -209,20 +212,27 @@ class ACMECertificateClient:
|
||||
def call_validate(
|
||||
self,
|
||||
pending_authzs: list[Authorization],
|
||||
*,
|
||||
get_challenge: t.Callable[[Authorization], str],
|
||||
wait: bool = True,
|
||||
) -> list[tuple[Authorization, str, Challenge | None]]:
|
||||
authzs_with_challenges_to_wait_for = []
|
||||
for authz in pending_authzs:
|
||||
challenge_type = get_challenge(authz)
|
||||
authz.call_validate(self.client, challenge_type, wait=wait)
|
||||
authz.call_validate(
|
||||
client=self.client, challenge_type=challenge_type, wait=wait
|
||||
)
|
||||
authzs_with_challenges_to_wait_for.append(
|
||||
(authz, challenge_type, authz.find_challenge(challenge_type))
|
||||
(
|
||||
authz,
|
||||
challenge_type,
|
||||
authz.find_challenge(challenge_type=challenge_type),
|
||||
)
|
||||
)
|
||||
return authzs_with_challenges_to_wait_for
|
||||
|
||||
def wait_for_validation(self, authzs_to_wait_for: list[Authorization]) -> None:
|
||||
wait_for_validation(authzs_to_wait_for, self.client)
|
||||
wait_for_validation(authzs=authzs_to_wait_for, client=self.client)
|
||||
|
||||
def _download_alternate_chains(
|
||||
self, cert: CertificateChain
|
||||
@@ -230,7 +240,7 @@ class ACMECertificateClient:
|
||||
alternate_chains = []
|
||||
for alternate in cert.alternates:
|
||||
try:
|
||||
alt_cert = CertificateChain.download(self.client, alternate)
|
||||
alt_cert = CertificateChain.download(client=self.client, url=alternate)
|
||||
except ModuleFailException as e:
|
||||
self.module.warn(
|
||||
f"Error while downloading alternative certificate {alternate}: {e}"
|
||||
@@ -275,7 +285,7 @@ class ACMECertificateClient:
|
||||
f"Order's crtificate URL {order.certificate_uri!r} is empty!"
|
||||
)
|
||||
|
||||
cert = CertificateChain.download(self.client, order.certificate_uri)
|
||||
cert = CertificateChain.download(client=self.client, url=order.certificate_uri)
|
||||
if cert.cert is None:
|
||||
raise ModuleFailException(
|
||||
f"Certificate at {order.certificate_uri} is empty!"
|
||||
@@ -314,22 +324,26 @@ class ACMECertificateClient:
|
||||
for identifier, authz in order.authorizations.items():
|
||||
if authz.status != "valid":
|
||||
authz.raise_error(
|
||||
f'Status is {authz.status!r} and not "valid"',
|
||||
error_msg=f'Status is {authz.status!r} and not "valid"',
|
||||
module=self.module,
|
||||
)
|
||||
|
||||
order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
|
||||
order.finalize(
|
||||
client=self.client,
|
||||
csr_der=pem_to_der(pem_filename=self.csr, pem_content=self.csr_content),
|
||||
)
|
||||
|
||||
return self.download_certificate(order, download_all_chains=download_all_chains)
|
||||
|
||||
def find_matching_chain(
|
||||
self,
|
||||
*,
|
||||
chains: list[CertificateChain],
|
||||
select_chain_matcher: t.Iterable[ChainMatcher],
|
||||
) -> CertificateChain | None:
|
||||
for criterium_idx, matcher in enumerate(select_chain_matcher):
|
||||
for chain in chains:
|
||||
if matcher.match(chain):
|
||||
if matcher.match(certificate=chain):
|
||||
self.module.debug(
|
||||
f"Found matching chain for criterium {criterium_idx}"
|
||||
)
|
||||
@@ -338,6 +352,7 @@ class ACMECertificateClient:
|
||||
|
||||
def write_cert_chain(
|
||||
self,
|
||||
*,
|
||||
cert: CertificateChain,
|
||||
cert_dest: str | os.PathLike | None = None,
|
||||
fullchain_dest: str | os.PathLike | None = None,
|
||||
@@ -347,18 +362,22 @@ class ACMECertificateClient:
|
||||
if cert.cert is None:
|
||||
raise ValueError("Certificate is not present")
|
||||
|
||||
if cert_dest and write_file(self.module, cert_dest, cert.cert.encode("utf8")):
|
||||
if cert_dest and write_file(
|
||||
module=self.module, dest=cert_dest, content=cert.cert.encode("utf8")
|
||||
):
|
||||
changed = True
|
||||
|
||||
if fullchain_dest and write_file(
|
||||
self.module,
|
||||
fullchain_dest,
|
||||
(cert.cert + "\n".join(cert.chain)).encode("utf8"),
|
||||
module=self.module,
|
||||
dest=fullchain_dest,
|
||||
content=(cert.cert + "\n".join(cert.chain)).encode("utf8"),
|
||||
):
|
||||
changed = True
|
||||
|
||||
if chain_dest and write_file(
|
||||
self.module, chain_dest, ("\n".join(cert.chain)).encode("utf8")
|
||||
module=self.module,
|
||||
dest=chain_dest,
|
||||
content=("\n".join(cert.chain)).encode("utf8"),
|
||||
):
|
||||
changed = True
|
||||
|
||||
@@ -374,7 +393,9 @@ class ACMECertificateClient:
|
||||
for authz_uri in order.authorization_uris:
|
||||
authz = None
|
||||
try:
|
||||
authz = Authorization.deactivate_url(self.client, authz_uri)
|
||||
authz = Authorization.deactivate_url(
|
||||
client=self.client, url=authz_uri
|
||||
)
|
||||
except Exception:
|
||||
# ignore errors
|
||||
pass
|
||||
@@ -385,7 +406,7 @@ class ACMECertificateClient:
|
||||
else:
|
||||
for authz in order.authorizations.values():
|
||||
try:
|
||||
authz.deactivate(self.client)
|
||||
authz.deactivate(client=self.client)
|
||||
except Exception:
|
||||
# ignore errors
|
||||
pass
|
||||
@@ -393,3 +414,6 @@ class ACMECertificateClient:
|
||||
self.module.warn(
|
||||
warning=f"Could not deactivate authz object {authz.url}."
|
||||
)
|
||||
|
||||
|
||||
__all__ = ("ACMECertificateClient",)
|
||||
|
||||
@@ -46,7 +46,7 @@ class CertificateChain:
|
||||
|
||||
@classmethod
|
||||
def download(
|
||||
cls: t.Type[_CertificateChain], client: ACMEClient, url: str
|
||||
cls: t.Type[_CertificateChain], *, client: ACMEClient, url: str
|
||||
) -> _CertificateChain:
|
||||
content, info = client.get_request(
|
||||
url,
|
||||
@@ -70,7 +70,10 @@ class CertificateChain:
|
||||
result.chain = certs[1:]
|
||||
|
||||
process_links(
|
||||
info, lambda link, relation: result._process_links(client, link, relation)
|
||||
info=info,
|
||||
callback=lambda link, relation: result._process_links(
|
||||
client=client, link=link, relation=relation
|
||||
),
|
||||
)
|
||||
|
||||
if result.cert is None:
|
||||
@@ -80,7 +83,7 @@ class CertificateChain:
|
||||
|
||||
return result
|
||||
|
||||
def _process_links(self, client: ACMEClient, link: str, relation: str) -> None:
|
||||
def _process_links(self, *, client: ACMEClient, link: str, relation: str) -> None:
|
||||
if relation == "up":
|
||||
# Process link-up headers if there was no chain in reply
|
||||
if not self.chain:
|
||||
@@ -105,7 +108,7 @@ class CertificateChain:
|
||||
|
||||
|
||||
class Criterium:
|
||||
def __init__(self, criterium: dict[str, t.Any], index: int):
|
||||
def __init__(self, *, criterium: dict[str, t.Any], index: int):
|
||||
self.index = index
|
||||
self.test_certificates: t.Literal["first", "last", "all"] = criterium[
|
||||
"test_certificates"
|
||||
@@ -120,7 +123,10 @@ class Criterium:
|
||||
|
||||
class ChainMatcher(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def match(self, certificate: CertificateChain) -> bool:
|
||||
def match(self, *, certificate: CertificateChain) -> bool:
|
||||
"""
|
||||
Check whether a certificate chain (CertificateChain instance) matches.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ("CertificateChain", "Criterium", "ChainMatcher")
|
||||
|
||||
@@ -34,7 +34,7 @@ if t.TYPE_CHECKING:
|
||||
)
|
||||
|
||||
|
||||
def create_key_authorization(client: ACMEClient, token: str) -> str:
|
||||
def create_key_authorization(*, client: ACMEClient, token: str) -> str:
|
||||
"""
|
||||
Returns the key authorization for the given token
|
||||
https://tools.ietf.org/html/rfc8555#section-8.1
|
||||
@@ -46,7 +46,7 @@ def create_key_authorization(client: ACMEClient, token: str) -> str:
|
||||
return f"{token}.{thumbprint}"
|
||||
|
||||
|
||||
def combine_identifier(identifier_type: str, identifier: str) -> str:
|
||||
def combine_identifier(*, identifier_type: str, identifier: str) -> str:
|
||||
return f"{identifier_type}:{identifier}"
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ def normalize_combined_identifier(identifier: str) -> str:
|
||||
identifier_type, identifier = split_identifier(identifier)
|
||||
# Normalize DNS names and IPs
|
||||
identifier = identifier.lower()
|
||||
return combine_identifier(identifier_type, identifier)
|
||||
return combine_identifier(identifier_type=identifier_type, identifier=identifier)
|
||||
|
||||
|
||||
def split_identifier(identifier: str) -> tuple[str, str]:
|
||||
@@ -70,7 +70,7 @@ _Challenge = t.TypeVar("_Challenge", bound="Challenge")
|
||||
|
||||
|
||||
class Challenge:
|
||||
def __init__(self, data: dict[str, t.Any], url: str) -> None:
|
||||
def __init__(self, *, data: dict[str, t.Any], url: str) -> None:
|
||||
self.data = data
|
||||
|
||||
self.type: str = data["type"]
|
||||
@@ -81,11 +81,12 @@ class Challenge:
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls: t.Type[_Challenge],
|
||||
*,
|
||||
client: ACMEClient,
|
||||
data: dict[str, t.Any],
|
||||
url: str | None = None,
|
||||
) -> _Challenge:
|
||||
return cls(data, url or data["url"])
|
||||
return cls(data=data, url=url or data["url"])
|
||||
|
||||
def call_validate(self, client: ACMEClient) -> None:
|
||||
challenge_response: dict[str, t.Any] = {}
|
||||
@@ -100,13 +101,13 @@ class Challenge:
|
||||
return self.data.copy()
|
||||
|
||||
def get_validation_data(
|
||||
self, client: ACMEClient, identifier_type: str, identifier: str
|
||||
self, *, client: ACMEClient, identifier_type: str, identifier: str
|
||||
) -> dict[str, t.Any] | None:
|
||||
if self.token is None:
|
||||
return None
|
||||
|
||||
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
|
||||
key_authorization = create_key_authorization(client, token)
|
||||
key_authorization = create_key_authorization(client=client, token=token)
|
||||
|
||||
if self.type == "http-01":
|
||||
# https://tools.ietf.org/html/rfc8555#section-8.3
|
||||
@@ -142,7 +143,9 @@ class Challenge:
|
||||
)
|
||||
return {
|
||||
"resource": resource,
|
||||
"resource_original": combine_identifier(identifier_type, identifier),
|
||||
"resource_original": combine_identifier(
|
||||
identifier_type=identifier_type, identifier=identifier
|
||||
),
|
||||
"resource_value": b_value,
|
||||
}
|
||||
|
||||
@@ -154,7 +157,7 @@ _Authorization = t.TypeVar("_Authorization", bound="Authorization")
|
||||
|
||||
|
||||
class Authorization:
|
||||
def __init__(self, url: str) -> None:
|
||||
def __init__(self, *, url: str) -> None:
|
||||
self.url = url
|
||||
|
||||
self.data: dict[str, t.Any] | None = None
|
||||
@@ -163,14 +166,14 @@ class Authorization:
|
||||
self.identifier_type: str | None = None
|
||||
self.identifier: str | None = None
|
||||
|
||||
def _setup(self, client: ACMEClient, data: dict[str, t.Any]) -> None:
|
||||
def _setup(self, *, client: ACMEClient, data: dict[str, t.Any]) -> None:
|
||||
data["uri"] = self.url
|
||||
self.data = data
|
||||
# While 'challenges' is a required field, apparently not every CA cares
|
||||
# (https://github.com/ansible-collections/community.crypto/issues/824)
|
||||
if data.get("challenges"):
|
||||
self.challenges = [
|
||||
Challenge.from_json(client, challenge)
|
||||
Challenge.from_json(client=client, data=challenge)
|
||||
for challenge in data["challenges"]
|
||||
]
|
||||
else:
|
||||
@@ -184,25 +187,27 @@ class Authorization:
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls: t.Type[_Authorization],
|
||||
*,
|
||||
client: ACMEClient,
|
||||
data: dict[str, t.Any],
|
||||
url: str,
|
||||
) -> _Authorization:
|
||||
result = cls(url)
|
||||
result._setup(client, data)
|
||||
result = cls(url=url)
|
||||
result._setup(client=client, data=data)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_url(
|
||||
cls: t.Type[_Authorization], client: ACMEClient, url: str
|
||||
cls: t.Type[_Authorization], *, client: ACMEClient, url: str
|
||||
) -> _Authorization:
|
||||
result = cls(url)
|
||||
result.refresh(client)
|
||||
result = cls(url=url)
|
||||
result.refresh(client=client)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls: t.Type[_Authorization],
|
||||
*,
|
||||
client: ACMEClient,
|
||||
identifier_type: str,
|
||||
identifier: str,
|
||||
@@ -220,7 +225,8 @@ class Authorization:
|
||||
}
|
||||
if "newAuthz" not in client.directory.directory:
|
||||
raise ACMEProtocolException(
|
||||
client.module, "ACME endpoint does not support pre-authorization"
|
||||
module=client.module,
|
||||
msg="ACME endpoint does not support pre-authorization",
|
||||
)
|
||||
url = client.directory["newAuthz"]
|
||||
|
||||
@@ -230,26 +236,28 @@ class Authorization:
|
||||
error_msg="Failed to request challenges",
|
||||
expected_status_codes=[200, 201],
|
||||
)
|
||||
return cls.from_json(client, result, info["location"])
|
||||
return cls.from_json(client=client, data=result, url=info["location"])
|
||||
|
||||
@property
|
||||
def combined_identifier(self) -> str:
|
||||
if self.identifier_type is None or self.identifier is None:
|
||||
raise ValueError("Data not present")
|
||||
return combine_identifier(self.identifier_type, self.identifier)
|
||||
return combine_identifier(
|
||||
identifier_type=self.identifier_type, identifier=self.identifier
|
||||
)
|
||||
|
||||
def to_json(self) -> dict[str, t.Any]:
|
||||
if self.data is None:
|
||||
raise ValueError("Data not present")
|
||||
return self.data.copy()
|
||||
|
||||
def refresh(self, client: ACMEClient) -> bool:
|
||||
def refresh(self, *, client: ACMEClient) -> bool:
|
||||
result, dummy = client.get_request(self.url)
|
||||
changed = self.data != result
|
||||
self._setup(client, result)
|
||||
self._setup(client=client, data=result)
|
||||
return changed
|
||||
|
||||
def get_challenge_data(self, client: ACMEClient) -> dict[str, t.Any]:
|
||||
def get_challenge_data(self, *, client: ACMEClient) -> dict[str, t.Any]:
|
||||
"""
|
||||
Returns a dict with the data for all proposed (and supported) challenges
|
||||
of the given authorization.
|
||||
@@ -259,13 +267,15 @@ class Authorization:
|
||||
data = {}
|
||||
for challenge in self.challenges:
|
||||
validation_data = challenge.get_validation_data(
|
||||
client, self.identifier_type, self.identifier
|
||||
client=client,
|
||||
identifier_type=self.identifier_type,
|
||||
identifier=self.identifier,
|
||||
)
|
||||
if validation_data is not None:
|
||||
data[challenge.type] = validation_data
|
||||
return data
|
||||
|
||||
def raise_error(self, error_msg: str, module: AnsibleModule) -> t.NoReturn:
|
||||
def raise_error(self, *, error_msg: str, module: AnsibleModule) -> t.NoReturn:
|
||||
"""
|
||||
Aborts with a specific error for a challenge.
|
||||
"""
|
||||
@@ -283,40 +293,40 @@ class Authorization:
|
||||
msg = f"{msg}: {problem}"
|
||||
error_details.append(msg)
|
||||
raise ACMEProtocolException(
|
||||
module,
|
||||
f"Failed to validate challenge for {self.combined_identifier}: {error_msg}. {'; '.join(error_details)}",
|
||||
module=module,
|
||||
msg=f"Failed to validate challenge for {self.combined_identifier}: {error_msg}. {'; '.join(error_details)}",
|
||||
extras=dict(
|
||||
identifier=self.combined_identifier,
|
||||
authorization=self.data,
|
||||
),
|
||||
)
|
||||
|
||||
def find_challenge(self, challenge_type: str) -> Challenge | None:
|
||||
def find_challenge(self, *, challenge_type: str) -> Challenge | None:
|
||||
for challenge in self.challenges:
|
||||
if challenge_type == challenge.type:
|
||||
return challenge
|
||||
return None
|
||||
|
||||
def wait_for_validation(self, client: ACMEClient, callenge_type: str) -> bool:
|
||||
def wait_for_validation(self, *, client: ACMEClient) -> bool:
|
||||
while True:
|
||||
self.refresh(client)
|
||||
self.refresh(client=client)
|
||||
if self.status in ["valid", "invalid", "revoked"]:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
if self.status == "invalid":
|
||||
self.raise_error('Status is "invalid"', module=client.module)
|
||||
self.raise_error(error_msg='Status is "invalid"', module=client.module)
|
||||
|
||||
return self.status == "valid"
|
||||
|
||||
def call_validate(
|
||||
self, client: ACMEClient, challenge_type: str, wait: bool = True
|
||||
self, *, client: ACMEClient, challenge_type: str, wait: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Validate the authorization provided in the auth dict. Returns True
|
||||
when the validation was successful and False when it was not.
|
||||
"""
|
||||
challenge = self.find_challenge(challenge_type)
|
||||
challenge = self.find_challenge(challenge_type=challenge_type)
|
||||
if challenge is None:
|
||||
raise ModuleFailException(
|
||||
f'Found no challenge of type "{challenge_type}" for identifier {self.combined_identifier}!'
|
||||
@@ -326,7 +336,7 @@ class Authorization:
|
||||
|
||||
if not wait:
|
||||
return self.status == "valid"
|
||||
return self.wait_for_validation(client, challenge_type)
|
||||
return self.wait_for_validation(client=client)
|
||||
|
||||
def can_deactivate(self) -> bool:
|
||||
"""
|
||||
@@ -336,7 +346,7 @@ class Authorization:
|
||||
"""
|
||||
return self.status in ("valid", "pending")
|
||||
|
||||
def deactivate(self, client: ACMEClient) -> bool | None:
|
||||
def deactivate(self, *, client: ACMEClient) -> bool | None:
|
||||
"""
|
||||
Deactivates this authorization.
|
||||
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||
@@ -355,35 +365,50 @@ class Authorization:
|
||||
|
||||
@classmethod
|
||||
def deactivate_url(
|
||||
cls: t.Type[_Authorization], client: ACMEClient, url: str
|
||||
cls: t.Type[_Authorization], *, client: ACMEClient, url: str
|
||||
) -> _Authorization:
|
||||
"""
|
||||
Deactivates this authorization.
|
||||
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
|
||||
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||
"""
|
||||
authz = cls(url)
|
||||
authz = cls(url=url)
|
||||
authz_deactivate = {"status": "deactivated"}
|
||||
result, info = client.send_signed_request(
|
||||
url, authz_deactivate, fail_on_error=True
|
||||
)
|
||||
authz._setup(client, result)
|
||||
authz._setup(client=client, data=result)
|
||||
return authz
|
||||
|
||||
|
||||
def wait_for_validation(authzs: t.Iterable[Authorization], client: ACMEClient) -> None:
|
||||
def wait_for_validation(
|
||||
*, authzs: t.Iterable[Authorization], client: ACMEClient
|
||||
) -> None:
|
||||
"""
|
||||
Wait until a list of authz is valid. Fail if at least one of them is invalid or revoked.
|
||||
"""
|
||||
while authzs:
|
||||
authzs_next = []
|
||||
for authz in authzs:
|
||||
authz.refresh(client)
|
||||
authz.refresh(client=client)
|
||||
if authz.status in ["valid", "invalid", "revoked"]:
|
||||
if authz.status != "valid":
|
||||
authz.raise_error('Status is not "valid"', module=client.module)
|
||||
authz.raise_error(
|
||||
error_msg='Status is not "valid"', module=client.module
|
||||
)
|
||||
else:
|
||||
authzs_next.append(authz)
|
||||
if authzs_next:
|
||||
time.sleep(2)
|
||||
authzs = authzs_next
|
||||
|
||||
|
||||
__all__ = (
|
||||
"create_key_authorization",
|
||||
"combine_identifier",
|
||||
"normalize_combined_identifier",
|
||||
"split_identifier",
|
||||
"Challenge",
|
||||
"Authorization",
|
||||
"wait_for_validation",
|
||||
)
|
||||
|
||||
@@ -25,7 +25,9 @@ def format_http_status(status_code: int) -> str:
|
||||
return f"{status_code} {expl}"
|
||||
|
||||
|
||||
def format_error_problem(problem: dict[str, t.Any], subproblem_prefix: str = "") -> str:
|
||||
def format_error_problem(
|
||||
problem: dict[str, t.Any], *, subproblem_prefix: str = ""
|
||||
) -> str:
|
||||
error_type = problem.get(
|
||||
"type", "about:blank"
|
||||
) # https://www.rfc-editor.org/rfc/rfc7807#section-3.1
|
||||
@@ -57,13 +59,14 @@ class ModuleFailException(Exception):
|
||||
self.msg = msg
|
||||
self.module_fail_args = args
|
||||
|
||||
def do_fail(self, module: AnsibleModule, **arguments) -> t.NoReturn:
|
||||
def do_fail(self, *, module: AnsibleModule, **arguments) -> t.NoReturn:
|
||||
module.fail_json(msg=self.msg, other=self.module_fail_args, **arguments)
|
||||
|
||||
|
||||
class ACMEProtocolException(ModuleFailException):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
module: AnsibleModule,
|
||||
msg: str | None = None,
|
||||
info: dict[str, t.Any] | None = None,
|
||||
@@ -168,3 +171,14 @@ class NetworkException(ModuleFailException):
|
||||
|
||||
class KeyParsingError(ModuleFailException):
|
||||
pass
|
||||
|
||||
|
||||
__all__ = (
|
||||
"format_http_status",
|
||||
"format_error_problem",
|
||||
"ModuleFailException",
|
||||
"ACMEProtocolException",
|
||||
"BackendException",
|
||||
"NetworkException",
|
||||
"KeyParsingError",
|
||||
)
|
||||
|
||||
@@ -33,7 +33,9 @@ def read_file(fn: str | os.PathLike) -> bytes:
|
||||
|
||||
|
||||
# This function was adapted from an earlier version of https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py
|
||||
def write_file(module: AnsibleModule, dest: str | os.PathLike, content: bytes) -> bool:
|
||||
def write_file(
|
||||
*, module: AnsibleModule, dest: str | os.PathLike, content: bytes
|
||||
) -> bool:
|
||||
"""
|
||||
Write content to destination file dest, only if the content
|
||||
has changed.
|
||||
@@ -95,3 +97,6 @@ def write_file(module: AnsibleModule, dest: str | os.PathLike, content: bytes) -
|
||||
)
|
||||
os.remove(tmpsrc)
|
||||
return changed
|
||||
|
||||
|
||||
__all__ = ("read_file", "write_file")
|
||||
|
||||
@@ -34,7 +34,7 @@ _Order = t.TypeVar("_Order", bound="Order")
|
||||
|
||||
|
||||
class Order:
|
||||
def __init__(self, url: str) -> None:
|
||||
def __init__(self, *, url: str) -> None:
|
||||
self.url = url
|
||||
|
||||
self.data: dict[str, t.Any] | None = None
|
||||
@@ -47,7 +47,7 @@ class Order:
|
||||
self.authorization_uris: list[str] = []
|
||||
self.authorizations: dict[str, Authorization] = {}
|
||||
|
||||
def _setup(self, client: ACMEClient, data: dict[str, t.Any]) -> None:
|
||||
def _setup(self, *, client: ACMEClient, data: dict[str, t.Any]) -> None:
|
||||
self.data = data
|
||||
|
||||
self.status = data["status"]
|
||||
@@ -62,21 +62,22 @@ class Order:
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls: t.Type[_Order], client: ACMEClient, data: dict[str, t.Any], url: str
|
||||
cls: t.Type[_Order], *, client: ACMEClient, data: dict[str, t.Any], url: str
|
||||
) -> _Order:
|
||||
result = cls(url)
|
||||
result._setup(client, data)
|
||||
result = cls(url=url)
|
||||
result._setup(client=client, data=data)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_url(cls: t.Type[_Order], client: ACMEClient, url: str) -> _Order:
|
||||
result = cls(url)
|
||||
result.refresh(client)
|
||||
def from_url(cls: t.Type[_Order], *, client: ACMEClient, url: str) -> _Order:
|
||||
result = cls(url=url)
|
||||
result.refresh(client=client)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls: t.Type[_Order],
|
||||
*,
|
||||
client: ACMEClient,
|
||||
identifiers: list[tuple[str, str]],
|
||||
replaces_cert_id: str | None = None,
|
||||
@@ -105,11 +106,12 @@ class Order:
|
||||
error_msg="Failed to start new order",
|
||||
expected_status_codes=[201],
|
||||
)
|
||||
return cls.from_json(client, result, info["location"])
|
||||
return cls.from_json(client=client, data=result, url=info["location"])
|
||||
|
||||
@classmethod
|
||||
def create_with_error_handling(
|
||||
cls: t.Type[_Order],
|
||||
*,
|
||||
client: ACMEClient,
|
||||
identifiers: list[tuple[str, str]],
|
||||
error_strategy: t.Literal[
|
||||
@@ -136,8 +138,8 @@ class Order:
|
||||
tries += 1
|
||||
try:
|
||||
return cls.create(
|
||||
client,
|
||||
identifiers,
|
||||
client=client,
|
||||
identifiers=identifiers,
|
||||
replaces_cert_id=replaces_cert_id,
|
||||
profile=profile,
|
||||
)
|
||||
@@ -164,34 +166,36 @@ class Order:
|
||||
|
||||
raise
|
||||
|
||||
def refresh(self, client: ACMEClient) -> bool:
|
||||
def refresh(self, *, client: ACMEClient) -> bool:
|
||||
result, dummy = client.get_request(self.url)
|
||||
changed = self.data != result
|
||||
self._setup(client, result)
|
||||
self._setup(client=client, data=result)
|
||||
return changed
|
||||
|
||||
def load_authorizations(self, client: ACMEClient) -> None:
|
||||
def load_authorizations(self, *, client: ACMEClient) -> None:
|
||||
for auth_uri in self.authorization_uris:
|
||||
authz = Authorization.from_url(client, auth_uri)
|
||||
authz = Authorization.from_url(client=client, url=auth_uri)
|
||||
self.authorizations[
|
||||
normalize_combined_identifier(authz.combined_identifier)
|
||||
] = authz
|
||||
|
||||
def wait_for_finalization(self, client: ACMEClient) -> None:
|
||||
def wait_for_finalization(self, *, client: ACMEClient) -> None:
|
||||
while True:
|
||||
self.refresh(client)
|
||||
self.refresh(client=client)
|
||||
if self.status in ["valid", "invalid", "pending", "ready"]:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
if self.status != "valid":
|
||||
raise ACMEProtocolException(
|
||||
client.module,
|
||||
f'Failed to wait for order to complete; got status "{self.status}"',
|
||||
module=client.module,
|
||||
msg=f'Failed to wait for order to complete; got status "{self.status}"',
|
||||
content_json=self.data,
|
||||
)
|
||||
|
||||
def finalize(self, client: ACMEClient, csr_der: bytes, wait: bool = True) -> None:
|
||||
def finalize(
|
||||
self, *, client: ACMEClient, csr_der: bytes, wait: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Create a new certificate based on the csr.
|
||||
Return the certificate object as dict
|
||||
@@ -212,13 +216,16 @@ class Order:
|
||||
# Instead of using the result, we call self.refresh(client) below.
|
||||
|
||||
if wait:
|
||||
self.wait_for_finalization(client)
|
||||
self.wait_for_finalization(client=client)
|
||||
else:
|
||||
self.refresh(client)
|
||||
self.refresh(client=client)
|
||||
if self.status not in ["procesing", "valid", "invalid"]:
|
||||
raise ACMEProtocolException(
|
||||
client.module,
|
||||
f'Failed to finalize order; got status "{self.status}"',
|
||||
module=client.module,
|
||||
msg=f'Failed to finalize order; got status "{self.status}"',
|
||||
info=info,
|
||||
content_json=result,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ("Order",)
|
||||
|
||||
@@ -48,7 +48,7 @@ def der_to_pem(der_cert: bytes) -> str:
|
||||
|
||||
|
||||
def pem_to_der(
|
||||
pem_filename: str | os.PathLike | None = None, pem_content: str | None = None
|
||||
*, pem_filename: str | os.PathLike | None = None, pem_content: str | None = None
|
||||
) -> bytes:
|
||||
"""
|
||||
Load PEM file, or use PEM file's content, and convert to DER.
|
||||
@@ -85,7 +85,7 @@ def pem_to_der(
|
||||
|
||||
|
||||
def process_links(
|
||||
info: dict[str, t.Any], callback: t.Callable[[str, str], None]
|
||||
*, info: dict[str, t.Any], callback: t.Callable[[str, str], None]
|
||||
) -> None:
|
||||
"""
|
||||
Process link header, calls callback for every link header with the URL and relation as options.
|
||||
@@ -100,6 +100,7 @@ def process_links(
|
||||
|
||||
def parse_retry_after(
|
||||
value: str,
|
||||
*,
|
||||
relative_with_timezone: bool = True,
|
||||
now: datetime.datetime | None = None,
|
||||
) -> datetime.datetime:
|
||||
@@ -112,7 +113,7 @@ def parse_retry_after(
|
||||
try:
|
||||
delta = datetime.timedelta(seconds=int(value))
|
||||
if now is None:
|
||||
now = get_now_datetime(relative_with_timezone)
|
||||
now = get_now_datetime(with_timezone=relative_with_timezone)
|
||||
return now + delta
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -126,6 +127,7 @@ def parse_retry_after(
|
||||
|
||||
|
||||
def compute_cert_id(
|
||||
*,
|
||||
backend: CryptoBackend,
|
||||
cert_info: CertificateInformation | None = None,
|
||||
cert_filename: str | os.PathLike | None = None,
|
||||
@@ -159,3 +161,13 @@ def compute_cert_id(
|
||||
|
||||
# Compose cert ID
|
||||
return f"{aki}.{serial}"
|
||||
|
||||
|
||||
__all__ = (
|
||||
"nopad_b64",
|
||||
"der_to_pem",
|
||||
"pem_to_der",
|
||||
"process_links",
|
||||
"parse_retry_after",
|
||||
"compute_cert_id",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user