Make all module_utils and plugin_utils private (#887)

* Add leading underscore. Remove deprecated module utils.

* Document module and plugin utils as private. Add changelog fragment.

* Convert relative to absolute imports.

* Remove unnecessary imports.
This commit is contained in:
Felix Fontein
2025-05-11 19:17:58 +02:00
committed by GitHub
parent f758d94fba
commit a5a4e022ba
146 changed files with 678 additions and 465 deletions

View File

@@ -0,0 +1,263 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import base64
import datetime
import os
import typing as t
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
CryptoBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
BackendException,
)
from ..test__time import TIMEZONES, cartesian_product
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
Criterium,
)
def load_fixture(name: str) -> str:
with open(
os.path.join(os.path.dirname(__file__), "fixtures", name), encoding="utf-8"
) as f:
return f.read()
TEST_PEM_DERS: list[tuple[str, bytes]] = [
(
load_fixture("privatekey_1.pem"),
base64.b64decode(
"MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAo"
"GCCqGSM49AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3"
"lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw=="
),
)
]
TEST_KEYS: list[tuple[str, dict[str, t.Any], str]] = [
(
load_fixture("privatekey_1.pem"),
{
"alg": "ES256",
"hash": "sha256",
"jwk": {
"crv": "P-256",
"kty": "EC",
"x": "AJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68E",
"y": "vEEs4V0egJkNyM2Q4pp001zu14VcpQ0_Ei8xOOPxKZs",
},
"point_size": 32,
"type": "ec",
},
load_fixture("privatekey_1.txt"),
)
]
TEST_CSRS: list[tuple[str, set[tuple[str, str]], str]] = [
(
load_fixture("csr_1.pem"),
set([("dns", "ansible.com"), ("dns", "example.com"), ("dns", "example.org")]),
load_fixture("csr_1.txt"),
),
(
load_fixture("csr_2.pem"),
set(
[
("dns", "ansible.com"),
("ip", "127.0.0.1"),
("ip", "::1"),
("ip", "2001:d88:ac10:fe01::"),
("ip", "2001:1234:5678:abcd:9876:5432:10fe:dcba"),
]
),
load_fixture("csr_2.txt"),
),
]
TEST_CERT = load_fixture("cert_1.pem")
TEST_CERT_2 = load_fixture("cert_2.pem")
TEST_CERT_OPENSSL_OUTPUT = load_fixture("cert_1.txt") # OpenSSL 3.3.0 output
TEST_CERT_OPENSSL_OUTPUT_2 = load_fixture("cert_2.txt") # OpenSSL 3.3.0 output
TEST_CERT_OPENSSL_OUTPUT_2B = load_fixture("cert_2-b.txt") # OpenSSL 1.1.1f output
TEST_CERT_DAYS: list[tuple[datetime.timedelta, datetime.datetime, int]] = (
cartesian_product(
TIMEZONES,
[
(datetime.datetime(2018, 11, 15, 1, 2, 3), 11),
(datetime.datetime(2018, 11, 25, 15, 20, 0), 1),
(datetime.datetime(2018, 11, 25, 15, 30, 0), 0),
],
)
)
TEST_CERT_INFO_1 = CertificateInformation(
not_valid_after=datetime.datetime(2018, 11, 26, 15, 28, 24),
not_valid_before=datetime.datetime(2018, 11, 25, 15, 28, 23),
serial_number=1,
subject_key_identifier=b"\x98\xd2\xfd\x3c\xcc\xcd\x69\x45\xfb\xe2\x8c\x30\x2c\x54\x62\x18\x34\xb7\x07\x73",
authority_key_identifier=None,
)
TEST_CERT_INFO_2 = CertificateInformation(
not_valid_before=datetime.datetime(2024, 5, 4, 20, 42, 21),
not_valid_after=datetime.datetime(2029, 5, 4, 20, 42, 20),
serial_number=4218235397573492796,
subject_key_identifier=b"\x17\xe5\x83\x22\x14\xef\x74\xd3\xbe\x7e\x30\x76\x56\x1f\x51\x74\x65\x1f\xe9\xf0",
authority_key_identifier=b"\x13\xc3\x4c\x3e\x59\x45\xdd\xe3\x63\x51\xa3\x46\x80\xc4\x08\xc7\x14\xc0\x64\x4e",
)
TEST_CERT_INFO: list[tuple[str, CertificateInformation, str]] = [
(TEST_CERT, TEST_CERT_INFO_1, TEST_CERT_OPENSSL_OUTPUT),
(TEST_CERT_2, TEST_CERT_INFO_2, TEST_CERT_OPENSSL_OUTPUT_2),
(TEST_CERT_2, TEST_CERT_INFO_2, TEST_CERT_OPENSSL_OUTPUT_2B),
]
TEST_PARSE_ACME_TIMESTAMP: list[tuple[datetime.timedelta, str, dict[str, int]]] = (
cartesian_product(
TIMEZONES,
[
(
"2024-01-01T00:11:22Z",
dict(year=2024, month=1, day=1, hour=0, minute=11, second=22),
),
(
"2024-01-01T00:11:22.123Z",
dict(
year=2024,
month=1,
day=1,
hour=0,
minute=11,
second=22,
microsecond=123000,
),
),
(
"2024-04-17T06:54:13.333333334Z",
dict(
year=2024,
month=4,
day=17,
hour=6,
minute=54,
second=13,
microsecond=333333,
),
),
(
"2024-01-01T00:11:22+0100",
dict(year=2023, month=12, day=31, hour=23, minute=11, second=22),
),
(
"2024-01-01T00:11:22.123+0100",
dict(
year=2023,
month=12,
day=31,
hour=23,
minute=11,
second=22,
microsecond=123000,
),
),
],
)
)
TEST_INTERPOLATE_TIMESTAMP: list[
tuple[datetime.timedelta, dict[str, int], dict[str, int], float, dict[str, int]]
] = cartesian_product(
TIMEZONES,
[
(
dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
0.0,
dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
),
(
dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
0.5,
dict(year=2024, month=1, day=1, hour=0, minute=30, second=0),
),
(
dict(year=2024, month=1, day=1, hour=0, minute=0, second=0),
dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
1.0,
dict(year=2024, month=1, day=1, hour=1, minute=0, second=0),
),
],
)
class FakeBackend(CryptoBackend):
def parse_key(
self,
key_file: str | os.PathLike | None = None,
key_content: str | None = None,
passphrase=None,
) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def sign(
self, payload64: str, protected64: str, key_data: dict[str, t.Any] | None
) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def create_mac_key(self, alg: str, key: str) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def get_ordered_csr_identifiers(
self,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def get_csr_identifiers(
self,
csr_filename: str | os.PathLike | None = None,
csr_content: str | bytes | None = None,
) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def get_cert_days(
self,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
now: datetime.datetime | None = None,
) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def create_chain_matcher(self, criterium: Criterium) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")
def get_cert_information(
self,
cert_filename: str | os.PathLike | None = None,
cert_content: str | bytes | None = None,
) -> t.NoReturn:
raise BackendException("Not implemented in fake backend")

View File

@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBljCCATugAwIBAgIBATAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwthbnNpYmxl
LmNvbTAeFw0xODExMjUxNTI4MjNaFw0xODExMjYxNTI4MjRaMBYxFDASBgNVBAMT
C2Fuc2libGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAJz0yAAXAwEm
OhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6AmQ3IzZDimnTTXO7XhVylDT8S
LzE44/Epm6N6MHgwIwYDVR0RBBwwGoILZXhhbXBsZS5jb22CC2V4YW1wbGUub3Jn
MAwGA1UdEwEB/wQCMAAwDwYDVR0PAQH/BAUDAweAADATBgNVHSUEDDAKBggrBgEF
BQcDATAdBgNVHQ4EFgQUmNL9PMzNaUX74owwLFRiGDS3B3MwCgYIKoZIzj0EAwID
SQAwRgIhALz7Ur96ky0OfM5D9MwFmCg2jccqm/UglGI9+4KeOEIyAiEAwFX4tdll
QSrd1HY/jMsHwdK5wH3JkK/9+fGwyRP11VI=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,38 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN=ansible.com
Validity
Not Before: Nov 25 15:28:23 2018 GMT
Not After : Nov 26 15:28:24 2018 GMT
Subject: CN=ansible.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
38:e3:f1:29:9b
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:example.org
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Key Identifier:
98:D2:FD:3C:CC:CD:69:45:FB:E2:8C:30:2C:54:62:18:34:B7:07:73
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:46:02:21:00:bc:fb:52:bf:7a:93:2d:0e:7c:ce:43:f4:cc:
05:98:28:36:8d:c7:2a:9b:f5:20:94:62:3d:fb:82:9e:38:42:
32:02:21:00:c0:55:f8:b5:d9:65:41:2a:dd:d4:76:3f:8c:cb:
07:c1:d2:b9:c0:7d:c9:90:af:fd:f9:f1:b0:c9:13:f5:d5:52

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,57 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4218235397573492796 (0x3a8a2ebeb358c03c)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = Pebble Intermediate CA 734609
Validity
Not Before: May 4 20:42:21 2024 GMT
Not After : May 4 20:42:20 2029 GMT
Subject: CN = example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (1024 bit)
Modulus:
00:c1:43:a5:f9:ad:00:b7:bb:1b:73:27:00:b3:a2:
4e:27:0d:ff:ae:64:3e:a0:7e:f9:28:56:48:47:21:
9e:0f:d8:fb:69:b5:21:e8:98:84:60:6c:aa:73:b9:
6e:d9:f6:19:ad:85:e0:c2:f6:80:d3:22:b8:5a:d6:
3a:89:3e:2a:7a:fc:1d:bf:fc:69:20:e5:91:b8:34:
52:26:c8:15:74:e1:36:0c:cd:ab:01:4a:ad:83:f5:
0b:77:96:31:cf:1c:ea:6f:88:75:23:ac:51:a6:d8:
77:43:1b:b3:44:93:2c:8d:05:25:fb:77:41:36:94:
81:d5:ca:56:ff:b5:23:b2:a5
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
17:E5:83:22:14:EF:74:D3:BE:7E:30:76:56:1F:51:74:65:1F:E9:F0
X509v3 Authority Key Identifier:
keyid:13:C3:4C:3E:59:45:DD:E3:63:51:A3:46:80:C4:08:C7:14:C0:64:4E
Authority Information Access:
OCSP - URI:http://10.88.0.74:5000/ocsp
X509v3 Subject Alternative Name:
DNS:example.com
Signature Algorithm: sha256WithRSAEncryption
31:43:de:b6:48:f4:b8:30:46:25:65:e6:91:22:33:1b:d1:ba:
3f:60:f8:c3:18:32:72:e9:f8:d1:88:11:5a:0a:86:dc:1d:6d:
a5:ea:58:cd:05:ea:cd:5e:40:86:c1:ae:d5:cd:2e:8a:ca:50:
ee:df:bd:cf:6c:d9:20:3b:4b:49:f8:d5:8a:e3:be:f3:dd:24:
b2:7f:3f:3b:bf:e6:8d:7a:f8:8f:4b:6e:25:60:80:33:6f:0f:
53:b7:7d:94:2a:d2:4a:db:3a:2f:70:79:d7:bf:05:ed:df:10:
61:e7:24:ac:b2:fc:03:bd:ad:8c:e1:f3:1d:cc:78:99:e3:22:
59:bf:c5:92:57:95:92:56:35:fc:05:8b:26:10:c5:1b:87:17:
64:0b:bd:33:a9:54:d5:c0:2b:43:56:1b:52:d3:4f:8b:6f:25:
06:58:7f:6f:aa:27:35:05:d5:57:6d:83:a0:73:de:40:3f:67:
1c:5a:92:c6:37:e6:8f:c7:b8:91:d7:50:b9:4d:d4:f2:92:1f:
8b:93:0c:e2:b4:b8:d7:1d:8e:ce:6d:19:dc:8f:12:8e:c0:f2:
92:3b:95:5a:8c:c8:69:0e:0b:f7:fa:1f:55:62:80:7c:e2:f6:
41:3f:7d:69:36:9e:7c:90:7e:d7:3b:e6:a3:15:de:a4:7d:95:
13:46:c6:1a

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDDjCCAfagAwIBAgIIOoouvrNYwDwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE
AxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA3MzQ2MDkwHhcNMjQwNTA0MjA0MjIx
WhcNMjkwNTA0MjA0MjIwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCBnzANBgkq
hkiG9w0BAQEFAAOBjQAwgYkCgYEAwUOl+a0At7sbcycAs6JOJw3/rmQ+oH75KFZI
RyGeD9j7abUh6JiEYGyqc7lu2fYZrYXgwvaA0yK4WtY6iT4qevwdv/xpIOWRuDRS
JsgVdOE2DM2rAUqtg/ULd5Yxzxzqb4h1I6xRpth3QxuzRJMsjQUl+3dBNpSB1cpW
/7UjsqUCAwEAAaOB0TCBzjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBflgyIU73TT
vn4wdlYfUXRlH+nwMB8GA1UdIwQYMBaAFBPDTD5ZRd3jY1GjRoDECMcUwGROMDcG
CCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovLzEwLjg4LjAuNzQ6NTAw
MC9vY3NwMBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IB
AQAxQ962SPS4MEYlZeaRIjMb0bo/YPjDGDJy6fjRiBFaCobcHW2l6ljNBerNXkCG
wa7VzS6KylDu373PbNkgO0tJ+NWK477z3SSyfz87v+aNeviPS24lYIAzbw9Tt32U
KtJK2zovcHnXvwXt3xBh5ySssvwDva2M4fMdzHiZ4yJZv8WSV5WSVjX8BYsmEMUb
hxdkC70zqVTVwCtDVhtS00+LbyUGWH9vqic1BdVXbYOgc95AP2ccWpLGN+aPx7iR
11C5TdTykh+LkwzitLjXHY7ObRncjxKOwPKSO5VajMhpDgv3+h9VYoB84vZBP31p
Np58kH7XO+ajFd6kfZUTRsYa
-----END CERTIFICATE-----

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,56 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4218235397573492796 (0x3a8a2ebeb358c03c)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Pebble Intermediate CA 734609
Validity
Not Before: May 4 20:42:21 2024 GMT
Not After : May 4 20:42:20 2029 GMT
Subject: CN=example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (1024 bit)
Modulus:
00:c1:43:a5:f9:ad:00:b7:bb:1b:73:27:00:b3:a2:
4e:27:0d:ff:ae:64:3e:a0:7e:f9:28:56:48:47:21:
9e:0f:d8:fb:69:b5:21:e8:98:84:60:6c:aa:73:b9:
6e:d9:f6:19:ad:85:e0:c2:f6:80:d3:22:b8:5a:d6:
3a:89:3e:2a:7a:fc:1d:bf:fc:69:20:e5:91:b8:34:
52:26:c8:15:74:e1:36:0c:cd:ab:01:4a:ad:83:f5:
0b:77:96:31:cf:1c:ea:6f:88:75:23:ac:51:a6:d8:
77:43:1b:b3:44:93:2c:8d:05:25:fb:77:41:36:94:
81:d5:ca:56:ff:b5:23:b2:a5
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
17:E5:83:22:14:EF:74:D3:BE:7E:30:76:56:1F:51:74:65:1F:E9:F0
X509v3 Authority Key Identifier:
13:C3:4C:3E:59:45:DD:E3:63:51:A3:46:80:C4:08:C7:14:C0:64:4E
Authority Information Access:
OCSP - URI:http://10.88.0.74:5000/ocsp
X509v3 Subject Alternative Name:
DNS:example.com
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
31:43:de:b6:48:f4:b8:30:46:25:65:e6:91:22:33:1b:d1:ba:
3f:60:f8:c3:18:32:72:e9:f8:d1:88:11:5a:0a:86:dc:1d:6d:
a5:ea:58:cd:05:ea:cd:5e:40:86:c1:ae:d5:cd:2e:8a:ca:50:
ee:df:bd:cf:6c:d9:20:3b:4b:49:f8:d5:8a:e3:be:f3:dd:24:
b2:7f:3f:3b:bf:e6:8d:7a:f8:8f:4b:6e:25:60:80:33:6f:0f:
53:b7:7d:94:2a:d2:4a:db:3a:2f:70:79:d7:bf:05:ed:df:10:
61:e7:24:ac:b2:fc:03:bd:ad:8c:e1:f3:1d:cc:78:99:e3:22:
59:bf:c5:92:57:95:92:56:35:fc:05:8b:26:10:c5:1b:87:17:
64:0b:bd:33:a9:54:d5:c0:2b:43:56:1b:52:d3:4f:8b:6f:25:
06:58:7f:6f:aa:27:35:05:d5:57:6d:83:a0:73:de:40:3f:67:
1c:5a:92:c6:37:e6:8f:c7:b8:91:d7:50:b9:4d:d4:f2:92:1f:
8b:93:0c:e2:b4:b8:d7:1d:8e:ce:6d:19:dc:8f:12:8e:c0:f2:
92:3b:95:5a:8c:c8:69:0e:0b:f7:fa:1f:55:62:80:7c:e2:f6:
41:3f:7d:69:36:9e:7c:90:7e:d7:3b:e6:a3:15:de:a4:7d:95:
13:46:c6:1a

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs
4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE
MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E
AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl
FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr
URnCJfTLr2T3
-----END CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,12 @@
cryptography 35.0.0 does not support the 'NEW' in there; so to fix tests we removed it.
Once cryptography is fixed we should revert to the old version.
-----BEGIN NEW CERTIFICATE REQUEST-----
MIIBJTCBzQIBADAWMRQwEgYDVQQDEwthbnNpYmxlLmNvbTBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABACc9MgAFwMBJjoU0ZI18cIHnW1juoKG2DN5VrM60uvBvEEs
4V0egJkNyM2Q4pp001zu14VcpQ0/Ei8xOOPxKZugVTBTBgkqhkiG9w0BCQ4xRjBE
MCMGA1UdEQQcMBqCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZzAMBgNVHRMBAf8E
AjAAMA8GA1UdDwEB/wQFAwMHgAAwCgYIKoZIzj0EAwIDRwAwRAIgcDyoRmwFVBDl
FvbFZtiSd5wmJU1ltM6JtcfnLWnjY54CICruOByrropFUkOKKb4xXOYsgaDT93Wr
URnCJfTLr2T3
-----END NEW CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,28 @@
Certificate Request:
Data:
Version: 1 (0x0)
Subject: CN = ansible.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
38:e3:f1:29:9b
ASN1 OID: prime256v1
NIST CURVE: P-256
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:example.com, DNS:example.org
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
Signature Algorithm: ecdsa-with-SHA256
30:44:02:20:70:3c:a8:46:6c:05:54:10:e5:16:f6:c5:66:d8:
92:77:9c:26:25:4d:65:b4:ce:89:b5:c7:e7:2d:69:e3:63:9e:
02:20:2a:ee:38:1c:ab:ae:8a:45:52:43:8a:29:be:31:5c:e6:
2c:81:a0:d3:f7:75:ab:51:19:c2:25:f4:cb:af:64:f7

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEqjCCApICAQAwADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANv1
V7gDsh76O//d9wclBcW6kNpWeR6eAggzThwbMZjcO7GFHQsBZCZGGVdyS37uhejc
RrIBdtDDWXhoh3Dz+GQxD+6GuwAEFyL1F3MfT0v1HHoO8fE74G5mD6+ZA2HRDeU9
jf8BPyVWHBtNbCmJGSlSNOFejWCmwvsLARQxqFBuTyRjgos4BkLyWMqZRukrzO1P
z7IBhuFrB608t+AG4vGnPXZNM7xefhzO8bPOiepT0YS2ERPkFmOy97SnwTGdKykw
ZYM9oKukYhE4Z+yOaTFpJMBNXwDCI5TMnhtc6eJrf5sOFH92n2E9+YWMoahUOiTw
G6XV5HfSpySpwORUaTITQRsPAM+bmK9f1jB6ctfFVwpa8uW/h8pSgbHgZvkeD6s6
rFLh9TQ24t0vrRmhnY7/AMFgbgJoBTBq0l0lEXS4FCGKDGqQOqSws+eHR/pHA4uY
v8d498SQl9fYsT/c7Uj3/JnMSRVN942yQUFCzwLf0/WzWCi2HTqPM8CPh5ryiJ30
GAN2eb026/noyTOXm479Tg9o86Tw9qczE0j0CdcRnr6J337RGHQg58PZ7j+hnUmK
wgyclyvjE10ZFBgToMGSnzYp5UeRcOFZ3bnK6LOsGC75mIvz2OQgSQeO5VQASEnO
9uhygNyo91sK4BtVroloit8ZCa82LlsHSCj/mMzPAgMBAAGgZTBjBgkqhkiG9w0B
CQ4xVjBUMFIGA1UdEQRLMEmCC2Fuc2libGUuY29thwR/AAABhxAAAAAAAAAAAAAA
AAAAAAABhxAgAQ2IrBD+AQAAAAAAAAAAhxAgARI0VnirzZh2VDIQ/ty6MA0GCSqG
SIb3DQEBCwUAA4ICAQBFRuANzVRcze+iur0YevjtYIXDa03GoWWkgnLuE8u8epTM
2248duG3TmvVvxWPN4iFrvFcZIvNsevBo+Z7kXJ24m3YldtXvwfAYmCZ062apSoh
yzgo3Q0KfDehwLcoJPe5bh+jbbgJVGGvJug/QFyHSVl+iGyFUXE7pwafl9LuNDi3
yfOYZLIQ34mBH4Rsvymj9xSTYliWDEEU/o7RrrZeEqkOxNeLh64LbnifdrYUputz
yBURg2xs9hpAsytZJX90iJW8aYPM1aQ7eetqTViIRoqUAmIQobnKlNnpOliBHl+p
RY+AtTnsfAetKUP7OsAZkHRTGAXx0JHJQ1ITY8w5Dcw/v1bDCbAfkDubBP3X+us9
RQk2h6m74hWFFNu9xOfkNejPf7h4gywfDjo/wGZFSWKyi6avB9V53znZgRUwc009
p5MM9e37MH8pyBqfnbSwOj4hUoyecRCIAFdywjMb9akP2u15XP3MOtJOEvecyCxN
TZBxupTg65zB47GeSAufnc8FaTZkE8xPuCtbvqOVOkWYqzlqNdCfK8f3AZdlpwLh
38wdUm5G7LIu6aQNiY66aQs9qVpoGvqdmxHRkuSwqwZxGgzcY1yJaWGXQ6R4jgC3
VKlMTUVs1WYV6jrYLHcVt6Rn/2FVTOns3Jn6cTPOdKViYoqF+yW8yCEAqAskZw==
-----END CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,78 @@
Certificate Request:
Data:
Version: 1 (0x0)
Subject:
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
Modulus:
00:db:f5:57:b8:03:b2:1e:fa:3b:ff:dd:f7:07:25:
05:c5:ba:90:da:56:79:1e:9e:02:08:33:4e:1c:1b:
31:98:dc:3b:b1:85:1d:0b:01:64:26:46:19:57:72:
4b:7e:ee:85:e8:dc:46:b2:01:76:d0:c3:59:78:68:
87:70:f3:f8:64:31:0f:ee:86:bb:00:04:17:22:f5:
17:73:1f:4f:4b:f5:1c:7a:0e:f1:f1:3b:e0:6e:66:
0f:af:99:03:61:d1:0d:e5:3d:8d:ff:01:3f:25:56:
1c:1b:4d:6c:29:89:19:29:52:34:e1:5e:8d:60:a6:
c2:fb:0b:01:14:31:a8:50:6e:4f:24:63:82:8b:38:
06:42:f2:58:ca:99:46:e9:2b:cc:ed:4f:cf:b2:01:
86:e1:6b:07:ad:3c:b7:e0:06:e2:f1:a7:3d:76:4d:
33:bc:5e:7e:1c:ce:f1:b3:ce:89:ea:53:d1:84:b6:
11:13:e4:16:63:b2:f7:b4:a7:c1:31:9d:2b:29:30:
65:83:3d:a0:ab:a4:62:11:38:67:ec:8e:69:31:69:
24:c0:4d:5f:00:c2:23:94:cc:9e:1b:5c:e9:e2:6b:
7f:9b:0e:14:7f:76:9f:61:3d:f9:85:8c:a1:a8:54:
3a:24:f0:1b:a5:d5:e4:77:d2:a7:24:a9:c0:e4:54:
69:32:13:41:1b:0f:00:cf:9b:98:af:5f:d6:30:7a:
72:d7:c5:57:0a:5a:f2:e5:bf:87:ca:52:81:b1:e0:
66:f9:1e:0f:ab:3a:ac:52:e1:f5:34:36:e2:dd:2f:
ad:19:a1:9d:8e:ff:00:c1:60:6e:02:68:05:30:6a:
d2:5d:25:11:74:b8:14:21:8a:0c:6a:90:3a:a4:b0:
b3:e7:87:47:fa:47:03:8b:98:bf:c7:78:f7:c4:90:
97:d7:d8:b1:3f:dc:ed:48:f7:fc:99:cc:49:15:4d:
f7:8d:b2:41:41:42:cf:02:df:d3:f5:b3:58:28:b6:
1d:3a:8f:33:c0:8f:87:9a:f2:88:9d:f4:18:03:76:
79:bd:36:eb:f9:e8:c9:33:97:9b:8e:fd:4e:0f:68:
f3:a4:f0:f6:a7:33:13:48:f4:09:d7:11:9e:be:89:
df:7e:d1:18:74:20:e7:c3:d9:ee:3f:a1:9d:49:8a:
c2:0c:9c:97:2b:e3:13:5d:19:14:18:13:a0:c1:92:
9f:36:29:e5:47:91:70:e1:59:dd:b9:ca:e8:b3:ac:
18:2e:f9:98:8b:f3:d8:e4:20:49:07:8e:e5:54:00:
48:49:ce:f6:e8:72:80:dc:a8:f7:5b:0a:e0:1b:55:
ae:89:68:8a:df:19:09:af:36:2e:5b:07:48:28:ff:
98:cc:cf
Exponent: 65537 (0x10001)
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:ansible.com, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, IP Address:2001:D88:AC10:FE01:0:0:0:0, IP Address:2001:1234:5678:ABCD:9876:5432:10FE:DCBA
Signature Algorithm: sha256WithRSAEncryption
45:46:e0:0d:cd:54:5c:cd:ef:a2:ba:bd:18:7a:f8:ed:60:85:
c3:6b:4d:c6:a1:65:a4:82:72:ee:13:cb:bc:7a:94:cc:db:6e:
3c:76:e1:b7:4e:6b:d5:bf:15:8f:37:88:85:ae:f1:5c:64:8b:
cd:b1:eb:c1:a3:e6:7b:91:72:76:e2:6d:d8:95:db:57:bf:07:
c0:62:60:99:d3:ad:9a:a5:2a:21:cb:38:28:dd:0d:0a:7c:37:
a1:c0:b7:28:24:f7:b9:6e:1f:a3:6d:b8:09:54:61:af:26:e8:
3f:40:5c:87:49:59:7e:88:6c:85:51:71:3b:a7:06:9f:97:d2:
ee:34:38:b7:c9:f3:98:64:b2:10:df:89:81:1f:84:6c:bf:29:
a3:f7:14:93:62:58:96:0c:41:14:fe:8e:d1:ae:b6:5e:12:a9:
0e:c4:d7:8b:87:ae:0b:6e:78:9f:76:b6:14:a6:eb:73:c8:15:
11:83:6c:6c:f6:1a:40:b3:2b:59:25:7f:74:88:95:bc:69:83:
cc:d5:a4:3b:79:eb:6a:4d:58:88:46:8a:94:02:62:10:a1:b9:
ca:94:d9:e9:3a:58:81:1e:5f:a9:45:8f:80:b5:39:ec:7c:07:
ad:29:43:fb:3a:c0:19:90:74:53:18:05:f1:d0:91:c9:43:52:
13:63:cc:39:0d:cc:3f:bf:56:c3:09:b0:1f:90:3b:9b:04:fd:
d7:fa:eb:3d:45:09:36:87:a9:bb:e2:15:85:14:db:bd:c4:e7:
e4:35:e8:cf:7f:b8:78:83:2c:1f:0e:3a:3f:c0:66:45:49:62:
b2:8b:a6:af:07:d5:79:df:39:d9:81:15:30:73:4d:3d:a7:93:
0c:f5:ed:fb:30:7f:29:c8:1a:9f:9d:b4:b0:3a:3e:21:52:8c:
9e:71:10:88:00:57:72:c2:33:1b:f5:a9:0f:da:ed:79:5c:fd:
cc:3a:d2:4e:12:f7:9c:c8:2c:4d:4d:90:71:ba:94:e0:eb:9c:
c1:e3:b1:9e:48:0b:9f:9d:cf:05:69:36:64:13:cc:4f:b8:2b:
5b:be:a3:95:3a:45:98:ab:39:6a:35:d0:9f:2b:c7:f7:01:97:
65:a7:02:e1:df:cc:1d:52:6e:46:ec:b2:2e:e9:a4:0d:89:8e:
ba:69:0b:3d:a9:5a:68:1a:fa:9d:9b:11:d1:92:e4:b0:ab:06:
71:1a:0c:dc:63:5c:89:69:61:97:43:a4:78:8e:00:b7:54:a9:
4c:4d:45:6c:d5:66:15:ea:3a:d8:2c:77:15:b7:a4:67:ff:61:
55:4c:e9:ec:dc:99:fa:71:33:ce:74:a5:62:62:8a:85:fb:25:
bc:c8:21:00:a8:0b:24:67

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDWajU0PyhYKeulfy/luNtkAve7DkwQ01bXJ97zbxB66oAoGCCqGSM49
AwEHoUQDQgAEAJz0yAAXAwEmOhTRkjXxwgedbWO6gobYM3lWszrS68G8QSzhXR6A
mQ3IzZDimnTTXO7XhVylDT8SLzE44/Epmw==
-----END EC PRIVATE KEY-----

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,14 @@
read EC key
Private-Key: (256 bit)
priv:
35:9a:8d:4d:0f:ca:16:0a:7a:e9:5f:cb:f9:6e:36:
d9:00:bd:ee:c3:93:04:34:d5:b5:c9:f7:bc:db:c4:
1e:ba
pub:
04:00:9c:f4:c8:00:17:03:01:26:3a:14:d1:92:35:
f1:c2:07:9d:6d:63:ba:82:86:d8:33:79:56:b3:3a:
d2:eb:c1:bc:41:2c:e1:5d:1e:80:99:0d:c8:cd:90:
e2:9a:74:d3:5c:ee:d7:85:5c:a5:0d:3f:12:2f:31:
38:e3:f1:29:9b
ASN1 OID: prime256v1
NIST CURVE: P-256

View File

@@ -0,0 +1,3 @@
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
SPDX-FileCopyrightText: Ansible Project

View File

@@ -0,0 +1,168 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import datetime
import typing as t
from unittest.mock import (
MagicMock,
)
import pytest
from ansible_collections.community.crypto.plugins.module_utils._acme.backend_cryptography import (
HAS_CURRENT_CRYPTOGRAPHY,
CryptographyBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
UTC,
ensure_utc_timezone,
)
from freezegun import freeze_time
from ..test__time import TIMEZONES
from .backend_data import (
TEST_CERT,
TEST_CERT_DAYS,
TEST_CERT_INFO,
TEST_CSRS,
TEST_INTERPOLATE_TIMESTAMP,
TEST_KEYS,
TEST_PARSE_ACME_TIMESTAMP,
)
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
)
if not HAS_CURRENT_CRYPTOGRAPHY:
pytest.skip("cryptography not found")
@pytest.mark.parametrize("pem, result, dummy", TEST_KEYS)
def test_eckeyparse_cryptography(
pem: str, result: dict[str, t.Any], dummy: str, tmpdir
) -> None:
fn = tmpdir / "test.pem"
fn.write(pem)
module = MagicMock()
backend = CryptographyBackend(module)
key = backend.parse_key(key_file=str(fn))
key.pop("key_obj")
assert key == result
key = backend.parse_key(key_content=pem)
key.pop("key_obj")
assert key == result
@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS)
def test_csridentifiers_cryptography(
csr: str, result: set[tuple[str, str]], openssl_output: str, tmpdir
) -> None:
fn = tmpdir / "test.csr"
fn.write(csr)
module = MagicMock()
backend = CryptographyBackend(module)
identifiers = backend.get_csr_identifiers(csr_filename=str(fn))
assert identifiers == result
identifiers = backend.get_csr_identifiers(csr_content=csr)
assert identifiers == result
@pytest.mark.parametrize("timezone, now, expected_days", TEST_CERT_DAYS)
def test_certdays_cryptography(
timezone: datetime.timedelta, now: datetime.datetime, expected_days: int, tmpdir
) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
fn = tmpdir / "test-cert.pem"
fn.write(TEST_CERT)
module = MagicMock()
backend = CryptographyBackend(module)
days = backend.get_cert_days(cert_filename=str(fn), now=now)
assert days == expected_days
days = backend.get_cert_days(cert_content=TEST_CERT, now=now)
assert days == expected_days
@pytest.mark.parametrize(
"cert_content, expected_cert_info, openssl_output", TEST_CERT_INFO
)
def test_get_cert_information(
cert_content: str,
expected_cert_info: CertificateInformation,
openssl_output: str,
tmpdir,
) -> None:
fn = tmpdir / "test-cert.pem"
fn.write(cert_content)
module = MagicMock()
backend = CryptographyBackend(module)
if CRYPTOGRAPHY_TIMEZONE:
expected_cert_info = expected_cert_info._replace(
not_valid_after=ensure_utc_timezone(expected_cert_info.not_valid_after),
not_valid_before=ensure_utc_timezone(expected_cert_info.not_valid_before),
)
cert_info = backend.get_cert_information(cert_filename=str(fn))
assert cert_info == expected_cert_info
cert_info = backend.get_cert_information(cert_content=cert_content)
assert cert_info == expected_cert_info
# @pytest.mark.parametrize("timezone", TIMEZONES)
# Due to a bug in freezegun (https://github.com/spulec/freezegun/issues/348, https://github.com/spulec/freezegun/issues/553)
# this only works with timezone = UTC if CRYPTOGRAPHY_TIMEZONE is truish
@pytest.mark.parametrize(
"timezone", [datetime.timedelta(hours=0)] if CRYPTOGRAPHY_TIMEZONE else TIMEZONES
)
def test_now(timezone: datetime.timedelta) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
module = MagicMock()
backend = CryptographyBackend(module)
now = backend.get_now()
if CRYPTOGRAPHY_TIMEZONE:
assert now.tzinfo is not None
assert now == datetime.datetime(2024, 2, 3, 4, 5, 6, tzinfo=UTC)
else:
assert now.tzinfo is None
assert now == datetime.datetime(2024, 2, 3, 4, 5, 6)
@pytest.mark.parametrize("timezone, input, expected", TEST_PARSE_ACME_TIMESTAMP)
def test_parse_acme_timestamp(
timezone: datetime.timedelta, input: str, expected: dict[str, int]
) -> None:
with freeze_time("2024-02-03 04:05:06 +00:00", tz_offset=timezone):
module = MagicMock()
backend = CryptographyBackend(module)
ts_expected = backend.get_utc_datetime(**expected)
timestamp = backend.parse_acme_timestamp(input)
assert ts_expected == timestamp
@pytest.mark.parametrize(
"timezone, start, end, percentage, expected", TEST_INTERPOLATE_TIMESTAMP
)
def test_interpolate_timestamp(
timezone: datetime.timedelta,
start: dict[str, int],
end: dict[str, int],
percentage: float,
expected: dict[str, int],
) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
module = MagicMock()
backend = CryptographyBackend(module)
ts_start = backend.get_utc_datetime(**start)
ts_end = backend.get_utc_datetime(**end)
ts_expected = backend.get_utc_datetime(**expected)
timestamp = backend.interpolate_timestamp(ts_start, ts_end, percentage)
assert ts_expected == timestamp

View File

@@ -0,0 +1,174 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import datetime
import typing as t
from unittest.mock import (
MagicMock,
)
import pytest
from ansible_collections.community.crypto.plugins.module_utils._acme.backend_openssl_cli import (
OpenSSLCLIBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._time import (
UTC,
ensure_utc_timezone,
)
from freezegun import freeze_time
from .backend_data import (
TEST_CERT,
TEST_CERT_DAYS,
TEST_CERT_INFO,
TEST_CERT_OPENSSL_OUTPUT,
TEST_CSRS,
TEST_INTERPOLATE_TIMESTAMP,
TEST_KEYS,
TEST_PARSE_ACME_TIMESTAMP,
)
if t.TYPE_CHECKING:
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
)
# from ..test_time import TIMEZONES
TEST_IPS = [
("0:0:0:0:0:0:0:1", "::1"),
("1::0:2", "1::2"),
("0000:0001:0000:0000:0000:0000:0000:0001", "0:1::1"),
("0000:0001:0000:0000:0001:0000:0000:0001", "0:1::1:0:0:1"),
("0000:0001:0000:0001:0000:0001:0000:0001", "0:1:0:1:0:1:0:1"),
("0.0.0.0", "0.0.0.0"),
("2001:d88:ac10:fe01:0:0:0:0", "2001:d88:ac10:fe01::"),
("0000:0000:0000:0000:0000:0000:0000:0000", "::"),
]
@pytest.mark.parametrize("pem, result, openssl_output", TEST_KEYS)
def test_eckeyparse_openssl(
pem: str, result: dict[str, t.Any], openssl_output: str, tmpdir
) -> None:
fn = tmpdir / "test.key"
fn.write(pem)
module = MagicMock()
module.run_command = MagicMock(return_value=(0, openssl_output, 0))
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
key = backend.parse_key(key_file=str(fn))
key.pop("key_file")
assert key == result
@pytest.mark.parametrize("csr, result, openssl_output", TEST_CSRS)
def test_csridentifiers_openssl(
csr: str, result: set[tuple[str, str]], openssl_output: str, tmpdir
) -> None:
fn = tmpdir / "test.csr"
fn.write(csr)
module = MagicMock()
module.run_command = MagicMock(return_value=(0, openssl_output, 0))
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
identifiers = backend.get_csr_identifiers(str(fn))
assert identifiers == result
@pytest.mark.parametrize("ip, result", TEST_IPS)
def test_normalize_ip(ip: str, result: str) -> None:
module = MagicMock()
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
assert backend._normalize_ip(ip) == result
@pytest.mark.parametrize("timezone, now, expected_days", TEST_CERT_DAYS)
def test_certdays_cryptography(
timezone: datetime.timedelta, now: datetime.datetime, expected_days: int, tmpdir
) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
fn = tmpdir / "test-cert.pem"
fn.write(TEST_CERT)
module = MagicMock()
module.run_command = MagicMock(return_value=(0, TEST_CERT_OPENSSL_OUTPUT, 0))
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
days = backend.get_cert_days(cert_filename=str(fn), now=now)
assert days == expected_days
days = backend.get_cert_days(cert_content=TEST_CERT, now=now)
assert days == expected_days
@pytest.mark.parametrize(
"cert_content, expected_cert_info, openssl_output", TEST_CERT_INFO
)
def test_get_cert_information(
cert_content: str,
expected_cert_info: CertificateInformation,
openssl_output: str,
tmpdir,
) -> None:
fn = tmpdir / "test-cert.pem"
fn.write(cert_content)
module = MagicMock()
module.run_command = MagicMock(return_value=(0, openssl_output, 0))
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
expected_cert_info = expected_cert_info._replace(
not_valid_after=ensure_utc_timezone(expected_cert_info.not_valid_after),
not_valid_before=ensure_utc_timezone(expected_cert_info.not_valid_before),
)
cert_info = backend.get_cert_information(cert_filename=str(fn))
assert cert_info == expected_cert_info
cert_info = backend.get_cert_information(cert_content=cert_content)
assert cert_info == expected_cert_info
# @pytest.mark.parametrize("timezone", TIMEZONES)
# Due to a bug in freezegun (https://github.com/spulec/freezegun/issues/348, https://github.com/spulec/freezegun/issues/553)
# this only works with timezone = UTC if CRYPTOGRAPHY_TIMEZONE is truish
@pytest.mark.parametrize("timezone", [datetime.timedelta(hours=0)])
def test_now(timezone: datetime.timedelta) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
module = MagicMock()
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
now = backend.get_now()
assert now.tzinfo is not None
assert now == datetime.datetime(2024, 2, 3, 4, 5, 6, tzinfo=UTC)
@pytest.mark.parametrize("timezone, input, expected", TEST_PARSE_ACME_TIMESTAMP)
def test_parse_acme_timestamp(
timezone: datetime.timedelta, input: str, expected: dict[str, int]
) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
module = MagicMock()
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
ts_expected = backend.get_utc_datetime(**expected)
timestamp = backend.parse_acme_timestamp(input)
assert ts_expected == timestamp
@pytest.mark.parametrize(
"timezone, start, end, percentage, expected", TEST_INTERPOLATE_TIMESTAMP
)
def test_interpolate_timestamp(
timezone: datetime.timedelta,
start: dict[str, int],
end: dict[str, int],
percentage: float,
expected: dict[str, int],
) -> None:
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):
module = MagicMock()
backend = OpenSSLCLIBackend(module, openssl_binary="openssl")
ts_start = backend.get_utc_datetime(**start)
ts_end = backend.get_utc_datetime(**end)
ts_expected = backend.get_utc_datetime(**expected)
timestamp = backend.interpolate_timestamp(ts_start, ts_end, percentage)
assert ts_expected == timestamp

View File

@@ -0,0 +1,214 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import typing as t
from unittest.mock import (
MagicMock,
)
import pytest
from ansible_collections.community.crypto.plugins.module_utils._acme.challenges import (
Authorization,
Challenge,
combine_identifier,
split_identifier,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ACMEProtocolException,
ModuleFailException,
)
def test_combine_identifier() -> None:
assert combine_identifier("", "") == ":"
assert combine_identifier("a", "b") == "a:b"
def test_split_identifier() -> None:
assert split_identifier(":") == ("", "")
assert split_identifier("a:b") == ("a", "b")
assert split_identifier("a:b:c") == ("a", "b:c")
with pytest.raises(ModuleFailException) as exc:
split_identifier("a")
assert exc.value.msg == 'Identifier "a" is not of the form <type>:<identifier>'
def test_challenge_from_to_json() -> None:
client = MagicMock()
data = {
"url": "xxx",
"type": "type",
"status": "valid",
}
client.version = 2
challenge = Challenge.from_json(client, data)
assert challenge.data == data
assert challenge.type == "type"
assert challenge.url == "xxx"
assert challenge.status == "valid"
assert challenge.token is None
assert challenge.to_json() == data
data = {
"type": "type",
"status": "valid",
"token": "foo",
}
challenge = Challenge.from_json(None, data, url="xxx") # type: ignore
assert challenge.data == data
assert challenge.type == "type"
assert challenge.url == "xxx"
assert challenge.status == "valid"
assert challenge.token == "foo"
assert challenge.to_json() == data
def test_authorization_from_to_json() -> None:
client = MagicMock()
client.version = 2
data: dict[str, t.Any]
data = {
"challenges": [],
"status": "valid",
"identifier": {
"type": "dns",
"value": "example.com",
},
}
authz = Authorization.from_json(client, data, "xxx")
assert authz.url == "xxx"
assert authz.status == "valid"
assert authz.identifier == "example.com"
assert authz.identifier_type == "dns"
assert authz.challenges == []
assert authz.to_json() == {
"uri": "xxx",
"challenges": [],
"status": "valid",
"identifier": {
"type": "dns",
"value": "example.com",
},
}
data = {
"challenges": [
{
"url": "xxxyyy",
"type": "type",
"status": "valid",
}
],
"status": "valid",
"identifier": {
"type": "dns",
"value": "example.com",
},
"wildcard": True,
}
authz = Authorization.from_json(client, data, "xxx")
assert authz.url == "xxx"
assert authz.status == "valid"
assert authz.identifier == "*.example.com"
assert authz.identifier_type == "dns"
assert len(authz.challenges) == 1
assert authz.challenges[0].data == {
"url": "xxxyyy",
"type": "type",
"status": "valid",
}
assert authz.to_json() == {
"uri": "xxx",
"challenges": [
{
"url": "xxxyyy",
"type": "type",
"status": "valid",
}
],
"status": "valid",
"identifier": {
"type": "dns",
"value": "example.com",
},
"wildcard": True,
}
def test_authorization_create_error() -> None:
client = MagicMock()
client.version = 2
client.directory.directory = {}
with pytest.raises(ACMEProtocolException) as exc:
Authorization.create(client, "dns", "example.com")
assert exc.value.msg == "ACME endpoint does not support pre-authorization."
def test_wait_for_validation_error() -> None:
client = MagicMock()
client.version = 2
data = {
"challenges": [
{
"url": "xxxyyy1",
"type": "dns-01",
"status": "invalid",
"error": {
"type": "dns-failed",
"subproblems": [
{
"type": "subproblem",
"detail": "example.com DNS-01 validation failed",
},
],
},
},
{
"url": "xxxyyy2",
"type": "http-01",
"status": "invalid",
"error": {
"type": "http-failed",
"subproblems": [
{
"type": "subproblem",
"detail": "example.com HTTP-01 validation failed",
},
],
},
},
{
"url": "xxxyyy3",
"type": "something-else",
"status": "valid",
},
],
"status": "invalid",
"identifier": {
"type": "dns",
"value": "example.com",
},
}
client.get_request = MagicMock(return_value=(data, {}))
authz = Authorization.from_json(client, data, "xxx")
with pytest.raises(ACMEProtocolException) as exc:
authz.wait_for_validation(client, "dns")
assert exc.value.msg == (
'Failed to validate challenge for dns:example.com: Status is "invalid". Challenge dns-01: Error dns-failed Subproblems:\n'
'(dns-01.0) Error subproblem: "example.com DNS-01 validation failed"; Challenge http-01: Error http-failed Subproblems:\n'
'(http-01.0) Error subproblem: "example.com HTTP-01 validation failed".'
)
data = data.copy()
data["uri"] = "xxx"
assert exc.value.module_fail_args == {
"identifier": "dns:example.com",
"authorization": data,
}

View File

@@ -0,0 +1,368 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import typing as t
from unittest.mock import (
MagicMock,
)
import pytest
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ACMEProtocolException,
format_error_problem,
)
TEST_FORMAT_ERROR_PROBLEM: list[tuple[dict[str, t.Any], str, str]] = [
(
{
"type": "foo",
},
"",
"Error foo",
),
({"type": "foo", "title": "bar"}, "", 'Error "bar" (foo)'),
({"type": "foo", "detail": "bar baz"}, "", 'Error foo: "bar baz"'),
({"type": "foo", "subproblems": []}, "", "Error foo Subproblems:"),
(
{
"type": "foo",
"subproblems": [
{
"type": "bar",
},
],
},
"",
"Error foo Subproblems:\n(0) Error bar",
),
(
{
"type": "foo",
"subproblems": [
{
"type": "bar",
"subproblems": [
{
"type": "baz",
},
],
},
],
},
"",
"Error foo Subproblems:\n(0) Error bar Subproblems:\n(0.0) Error baz",
),
(
{
"type": "foo",
"title": "Foo Error",
"detail": "Foo went wrong",
"subproblems": [
{
"type": "bar",
"detail": "Bar went wrong",
"subproblems": [
{
"type": "baz",
"title": "Baz Error",
},
],
},
{
"type": "bar2",
"title": "Bar 2 Error",
"detail": "Bar really went wrong",
},
],
},
"X.",
'Error "Foo Error" (foo): "Foo went wrong" Subproblems:\n'
'(X.0) Error bar: "Bar went wrong" Subproblems:\n'
'(X.0.0) Error "Baz Error" (baz)\n'
'(X.1) Error "Bar 2 Error" (bar2): "Bar really went wrong"',
),
]
@pytest.mark.parametrize(
"problem, subproblem_prefix, result", TEST_FORMAT_ERROR_PROBLEM
)
def test_format_error_problem(
problem: dict[str, t.Any], subproblem_prefix: str, result: str
) -> None:
res = format_error_problem(problem, subproblem_prefix)
assert res == result
def create_regular_response(response_text: str) -> MagicMock:
response = MagicMock()
response.read = MagicMock(return_value=response_text.encode("utf-8"))
response.closed = False
return response
def create_error_response() -> MagicMock:
response = MagicMock()
response.read = MagicMock(side_effect=AttributeError("read"))
response.closed = True
return response
def create_decode_error(msg: str) -> t.Callable[[t.Any], t.Any]:
def f(content: t.Any) -> t.NoReturn:
raise Exception(msg)
return f
TEST_ACME_PROTOCOL_EXCEPTION: list[
tuple[dict[str, t.Any], t.Callable[[t.Any], t.Any] | None, str, dict[str, t.Any]]
] = [
(
{},
None,
"ACME request failed.",
{},
),
(
{
"msg": "Foo",
"extras": {
"foo": "bar",
},
},
None,
"Foo.",
{
"foo": "bar",
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
},
None,
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created.",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
"response": create_regular_response("xxx"),
},
None,
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The raw error result: xxx",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
"response": create_regular_response("xxx"),
},
create_decode_error("yyy"),
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The raw error result: xxx",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
"response": create_regular_response("xxx"),
},
lambda content: dict(foo="bar"),
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The JSON error result: {'foo': 'bar'}",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
"response": create_error_response(),
},
None,
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created.",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
"body": "xxx",
},
"response": create_error_response(),
},
lambda content: dict(foo="bar"),
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The JSON error result: {'foo': 'bar'}",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
"content": "xxx",
},
None,
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The raw error result: xxx",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 400,
},
"content_json": {
"foo": "bar",
},
"extras": {
"bar": "baz",
},
},
None,
"ACME request failed for https://ca.example.com/foo with HTTP status 400 Bad Request. The JSON error result: {'foo': 'bar'}",
{
"http_url": "https://ca.example.com/foo",
"http_status": 400,
"bar": "baz",
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 201,
},
"content_json": {
"type": "foo",
},
},
None,
"ACME request failed for https://ca.example.com/foo with HTTP status 201 Created. The JSON error result: {'type': 'foo'}",
{
"http_url": "https://ca.example.com/foo",
"http_status": 201,
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 400,
},
"content_json": {
"type": "foo",
},
},
None,
"ACME request failed for https://ca.example.com/foo with status 400 Bad Request. Error foo.",
{
"http_url": "https://ca.example.com/foo",
"http_status": 400,
"problem": {
"type": "foo",
},
"subproblems": [],
},
),
(
{
"info": {
"url": "https://ca.example.com/foo",
"status": 400,
},
"content_json": {
"type": "foo",
"title": "Foo Error",
"subproblems": [
{
"type": "bar",
"detail": "This is a bar error",
"details": "Details.",
},
],
},
},
None,
'ACME request failed for https://ca.example.com/foo with status 400 Bad Request. Error "Foo Error" (foo). Subproblems:\n'
'(0) Error bar: "This is a bar error".',
{
"http_url": "https://ca.example.com/foo",
"http_status": 400,
"problem": {
"type": "foo",
"title": "Foo Error",
},
"subproblems": [
{
"type": "bar",
"detail": "This is a bar error",
"details": "Details.",
},
],
},
),
]
@pytest.mark.parametrize("input, from_json, msg, args", TEST_ACME_PROTOCOL_EXCEPTION)
def test_acme_protocol_exception(
input: dict[str, t.Any],
from_json: t.Callable[[t.Any], t.NoReturn] | None,
msg: str,
args: dict[str, t.Any],
) -> None:
if from_json is None:
module = None
else:
module = MagicMock()
module.from_json = from_json
with pytest.raises(ACMEProtocolException) as exc:
raise ACMEProtocolException(module, **input) # type: ignore
print(exc.value.msg)
print(exc.value.module_fail_args)
print(msg)
print(args)
assert exc.value.msg == msg
assert exc.value.module_fail_args == args

View File

@@ -0,0 +1,31 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
from unittest.mock import (
MagicMock,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.io import (
read_file,
write_file,
)
TEST_TEXT = r"""1234
5678"""
def test_read_file(tmpdir) -> None:
fn = tmpdir / "test.txt"
fn.write(TEST_TEXT)
assert read_file(str(fn)) == TEST_TEXT.encode("utf-8")
def test_write_file(tmpdir) -> None:
fn = tmpdir / "test.txt"
module = MagicMock()
write_file(module, str(fn), TEST_TEXT.encode("utf-8"))
assert fn.read() == TEST_TEXT

View File

@@ -0,0 +1,56 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
from unittest.mock import (
MagicMock,
)
import pytest
from ansible_collections.community.crypto.plugins.module_utils._acme.errors import (
ACMEProtocolException,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.orders import Order
def test_order_from_json() -> None:
client = MagicMock()
data = {
"status": "valid",
"identifiers": [],
"authorizations": [],
}
client.version = 2
order = Order.from_json(client, data, "xxx")
assert order.data == data
assert order.url == "xxx"
assert order.status == "valid"
assert order.identifiers == []
assert order.finalize_uri is None
assert order.certificate_uri is None
assert order.authorization_uris == []
assert order.authorizations == {}
def test_wait_for_finalization_error() -> None:
client = MagicMock()
client.version = 2
data = {
"status": "invalid",
"identifiers": [],
"authorizations": [],
}
order = Order.from_json(client, data, "xxx")
client.get_request = MagicMock(return_value=(data, {}))
with pytest.raises(ACMEProtocolException) as exc:
order.wait_for_finalization(client)
assert exc.value.msg.startswith(
'Failed to wait for order to complete; got status "invalid". The JSON result: '
)
assert exc.value.module_fail_args == {}

View File

@@ -0,0 +1,135 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import datetime
import typing as t
import pytest
from ansible_collections.community.crypto.plugins.module_utils._acme.backends import (
CertificateInformation,
CryptoBackend,
)
from ansible_collections.community.crypto.plugins.module_utils._acme.utils import (
compute_cert_id,
nopad_b64,
parse_retry_after,
pem_to_der,
process_links,
)
from .backend_data import TEST_PEM_DERS
NOPAD_B64: list[tuple[str, str]] = [
("", ""),
("\n", "Cg"),
("123", "MTIz"),
("Lorem?ipsum", "TG9yZW0_aXBzdW0"),
]
TEST_LINKS_HEADER: list[tuple[dict[str, t.Any], list[tuple[str, str]]]] = [
(
{},
[],
),
(
{"link": '<foo>; rel="bar"'},
[
("foo", "bar"),
],
),
(
{"link": '<foo>; rel="bar", <baz>; rel="bam"'},
[
("foo", "bar"),
("baz", "bam"),
],
),
(
{
"link": '<https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preconnect", <https://three.example.com>; rel="preconnect"'
},
[
("https://one.example.com", "preconnect"),
("https://two.example.com", "preconnect"),
("https://three.example.com", "preconnect"),
],
),
]
TEST_RETRY_AFTER_HEADER: list[tuple[str, datetime.datetime]] = [
("120", datetime.datetime(2024, 4, 29, 0, 2, 0)),
("Wed, 21 Oct 2015 07:28:00 GMT", datetime.datetime(2015, 10, 21, 7, 28, 0)),
]
TEST_COMPUTE_CERT_ID: list[tuple[CertificateInformation, str]] = [
(
CertificateInformation(
not_valid_after=datetime.datetime(2018, 11, 26, 15, 28, 24),
not_valid_before=datetime.datetime(2018, 11, 25, 15, 28, 23),
serial_number=1,
subject_key_identifier=None,
authority_key_identifier=b"\x00\xff",
),
"AP8.AQ",
),
(
# AKI, serial number, and expected result taken from
# https://letsencrypt.org/2024/04/25/guide-to-integrating-ari-into-existing-acme-clients.html#step-3-constructing-the-ari-certid
CertificateInformation(
not_valid_after=datetime.datetime(2018, 11, 26, 15, 28, 24),
not_valid_before=datetime.datetime(2018, 11, 25, 15, 28, 23),
serial_number=0x87654321,
subject_key_identifier=None,
authority_key_identifier=b"\x69\x88\x5b\x6b\x87\x46\x40\x41\xe1\xb3\x7b\x84\x7b\xa0\xae\x2c\xde\x01\xc8\xd4",
),
"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE",
),
]
@pytest.mark.parametrize("value, result", NOPAD_B64)
def test_nopad_b64(value: str, result: str) -> None:
assert nopad_b64(value.encode("utf-8")) == result
@pytest.mark.parametrize("pem, der", TEST_PEM_DERS)
def test_pem_to_der(pem: str, der: bytes, tmpdir):
fn = tmpdir / "test.pem"
fn.write(pem)
assert pem_to_der(str(fn)) == der
@pytest.mark.parametrize("value, expected_result", TEST_LINKS_HEADER)
def test_process_links(
value: dict[str, t.Any], expected_result: list[tuple[str, str]]
) -> None:
data = []
def callback(url, rel):
data.append((url, rel))
process_links(value, callback)
assert expected_result == data
@pytest.mark.parametrize("value, expected_result", TEST_RETRY_AFTER_HEADER)
def test_parse_retry_after(value: str, expected_result: datetime.datetime) -> None:
assert expected_result == parse_retry_after(
value, now=datetime.datetime(2024, 4, 29, 0, 0, 0)
)
@pytest.mark.parametrize("cert_info, expected_result", TEST_COMPUTE_CERT_ID)
def test_compute_cert_id(
cert_info: CertificateInformation, expected_result: str
) -> None:
backend: CryptoBackend = None # type: ignore
assert expected_result == compute_cert_id(backend=backend, cert_info=cert_info)