ACME: improve acme_certificate docs, include cert_id in acme_certificate_renewal_info return value (#747)

* Use community.dns.quote_txt filter instead of regex replace to quote TXT entry value.

* Fix documentation of acme_certificate's challenge_data return value.

* Also return cert_id from acme_certificate_renewal_info module.

* The cert ID cannot be computed if the certificate has no AKI.

This happens with older Pebble versions, which are used when
testing against older ansible-core/-base/Ansible versions.

* Fix AKI extraction for older OpenSSL versions.
pull/748/head
Felix Fontein 2024-05-04 23:38:57 +02:00 committed by GitHub
parent 59606d48ad
commit 553ab45f46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 323 additions and 115 deletions

View File

@ -390,6 +390,7 @@ class ACMEClient(object):
def get_renewal_info(
self,
cert_id=None,
cert_info=None,
cert_filename=None,
cert_content=None,
@ -399,7 +400,8 @@ class ACMEClient(object):
if not self.directory.has_renewal_info_endpoint():
raise ModuleFailException('The ACME endpoint does not support ACME Renewal Information retrieval')
cert_id = compute_cert_id(self.backend, cert_info=cert_info, cert_filename=cert_filename, cert_content=cert_content)
if cert_id is None:
cert_id = compute_cert_id(self.backend, cert_info=cert_info, cert_filename=cert_filename, cert_content=cert_content)
url = '{base}{cert_id}'.format(base=self.directory.directory['renewalInfo'], cert_id=cert_id)
data, info = self.get_request(url, parse_json_result=True, fail_on_error=True, get_only=True)

View File

@ -56,12 +56,12 @@ def _decode_octets(octets_text):
return binascii.unhexlify(re.sub(r"(\s|:)", "", octets_text).encode("utf-8"))
def _extract_octets(out_text, name, required=True):
match = re.search(
r"\s+%s:\s*\n\s+([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2})*)\s*\n" % name,
out_text,
re.MULTILINE | re.DOTALL,
def _extract_octets(out_text, name, required=True, potential_prefixes=None):
regexp = r"\s+%s:\s*\n\s+%s([A-Fa-f0-9]{2}(?::[A-Fa-f0-9]{2})*)\s*\n" % (
name,
('(?:%s)' % '|'.join(re.escape(pp) for pp in potential_prefixes)) if potential_prefixes else '',
)
match = re.search(regexp, out_text, re.MULTILINE | re.DOTALL)
if match is not None:
return _decode_octets(match.group(1))
if not required:
@ -379,7 +379,7 @@ class OpenSSLCLIBackend(CryptoBackend):
serial = convert_bytes_to_int(_extract_octets(out_text, 'Serial Number', required=True))
ski = _extract_octets(out_text, 'X509v3 Subject Key Identifier', required=False)
aki = _extract_octets(out_text, 'X509v3 Authority Key Identifier', required=False)
aki = _extract_octets(out_text, 'X509v3 Authority Key Identifier', required=False, potential_prefixes=['keyid:', ''])
return CertificateInformation(
not_valid_after=not_after,

View File

@ -109,7 +109,7 @@ def compute_cert_id(backend, cert_info=None, cert_filename=None, cert_content=No
# Convert Authority Key Identifier to string
if cert_info.authority_key_identifier is None:
raise ModuleFailException('Module has no Authority Key Identifier extension')
raise ModuleFailException('Certificate has no Authority Key Identifier extension')
aki = to_native(base64.urlsafe_b64encode(cert_info.authority_key_identifier)).replace('=', '')
# Convert serial number to string

View File

@ -404,7 +404,7 @@ EXAMPLES = r'''
# state: present
# wait: true
# # Note: route53 requires TXT entries to be enclosed in quotes
# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}"
# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | community.dns.quote_txt(always_quote=true) }}"
# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge.challenge_data
#
# Alternative way:
@ -419,7 +419,7 @@ EXAMPLES = r'''
# wait: true
# # Note: item.value is a list of TXT entries, and route53
# # requires every entry to be enclosed in quotes
# value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}"
# value: "{{ item.value | map('community.dns.quote_txt', always_quote=true) | list }}"
# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
# when: sample_com_challenge is changed
@ -475,39 +475,55 @@ challenge_data:
- Per identifier / challenge type challenge data.
- Since Ansible 2.8.5, only challenges which are not yet valid are returned.
returned: changed
type: list
elements: dict
type: dict
contains:
resource:
description: The challenge resource that must be created for validation.
returned: changed
type: str
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_original:
identifier:
description:
- The original challenge resource including type identifier for V(tls-alpn-01)
challenges.
returned: changed and O(challenge) is V(tls-alpn-01)
type: str
sample: DNS:example.com
resource_value:
description:
- The value the resource has to produce for the validation.
- For V(http-01) and V(dns-01) challenges, the value can be used as-is.
- "For V(tls-alpn-01) challenges, note that this return value contains a
Base64 encoded version of the correct binary blob which has to be put
into the acmeValidation x509 extension; see
U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3)
for details. To do this, you might need the P(ansible.builtin.b64decode#filter) Jinja filter
to extract the binary blob from this return value."
- For every identifier, provides a dictionary of challenge types mapping to challenge data.
- The keys in this dictionary the identifiers. C(identifier) is a placeholder used in the documentation.
- Note that the keys are not valid Jinja2 identifiers.
returned: changed
type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
record:
description: The full DNS record's name for the challenge.
returned: changed and challenge is V(dns-01)
type: str
sample: _acme-challenge.example.com
type: dict
contains:
challenge-type:
description:
- Data for every challenge type.
- The keys in this dictionary the challenge types. C(challenge-type) is a placeholder used in the documentation.
Possible keys are V(http-01), V(dns-01), and V(tls-alpn-01).
- Note that the keys are not valid Jinja2 identifiers.
returned: changed
type: dict
contains:
resource:
description: The challenge resource that must be created for validation.
returned: changed
type: str
sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
resource_original:
description:
- The original challenge resource including type identifier for V(tls-alpn-01)
challenges.
returned: changed and O(challenge) is V(tls-alpn-01)
type: str
sample: DNS:example.com
resource_value:
description:
- The value the resource has to produce for the validation.
- For V(http-01) and V(dns-01) challenges, the value can be used as-is.
- "For V(tls-alpn-01) challenges, note that this return value contains a
Base64 encoded version of the correct binary blob which has to be put
into the acmeValidation x509 extension; see
U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3)
for details. To do this, you might need the P(ansible.builtin.b64decode#filter) Jinja filter
to extract the binary blob from this return value."
returned: changed
type: str
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
record:
description: The full DNS record's name for the challenge.
returned: changed and challenge is V(dns-01)
type: str
sample: _acme-challenge.example.com
challenge_data_dns:
description:
- List of TXT values per DNS record, in case challenge is V(dns-01).

View File

@ -119,6 +119,13 @@ supports_ari:
returned: success
type: bool
sample: true
cert_id:
description:
- The certificate ID according to the L(ARI specification, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-4.1).
returned: success, the certificate exists, and has an Authority Key Identifier X.509 extension
type: str
sample: aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE
'''
import os
@ -134,6 +141,8 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.acme import
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import compute_cert_id
def main():
argument_spec = get_default_argspec(with_account=False)
@ -155,109 +164,86 @@ def main():
)
backend = create_backend(module, True)
result = dict(
changed=False,
msg='The certificate is still valid and no condition was reached',
supports_ari=False,
)
def complete(should_renew, **kwargs):
result['should_renew'] = should_renew
result.update(kwargs)
module.exit_json(**result)
if not module.params['certificate_path'] and not module.params['certificate_content']:
module.exit_json(
changed=False,
should_renew=True,
msg='No certificate was specified',
supports_ari=False,
)
complete(True, msg='No certificate was specified')
if module.params['certificate_path'] is not None and not os.path.exists(module.params['certificate_path']):
module.exit_json(
changed=False,
should_renew=True,
msg='The certificate file does not exist',
supports_ari=False,
)
complete(True, msg='The certificate file does not exist')
try:
cert_info = backend.get_cert_information(
cert_filename=module.params['certificate_path'],
cert_content=module.params['certificate_content'],
)
cert_id = None
if cert_info.authority_key_identifier is not None:
cert_id = compute_cert_id(backend, cert_info=cert_info)
if cert_id is not None:
result['cert_id'] = cert_id
if module.params['now']:
now = backend.parse_module_parameter(module.params['now'], 'now')
else:
now = backend.get_now()
no_renewal_msg = 'The certificate is still valid and no condition was reached'
renewal_ari = False
if now >= cert_info.not_valid_after:
module.exit_json(
changed=False,
should_renew=True,
msg='The certificate already expired',
supports_ari=False,
)
complete(True, msg='The certificate has already expired')
client = ACMEClient(module, backend)
if client.directory.has_renewal_info_endpoint():
renewal_info = client.get_renewal_info(cert_info=cert_info)
if cert_id is not None and module.params['use_ari'] and client.directory.has_renewal_info_endpoint():
renewal_info = client.get_renewal_info(cert_id=cert_id)
window_start = backend.parse_acme_timestamp(renewal_info['suggestedWindow']['start'])
window_end = backend.parse_acme_timestamp(renewal_info['suggestedWindow']['end'])
msg_append = ''
if 'explanationURL' in renewal_info:
msg_append = '. Information on renewal interval: {0}'.format(renewal_info['explanationURL'])
renewal_ari = True
result['supports_ari'] = True
if now > window_end:
module.exit_json(
changed=False,
should_renew=True,
msg='The suggested renewal interval provided by ARI is in the past{0}'.format(msg_append),
supports_ari=True,
)
complete(True, msg='The suggested renewal interval provided by ARI is in the past{0}'.format(msg_append))
if module.params['ari_algorithm'] == 'start':
if now > window_start:
module.exit_json(
changed=False,
should_renew=True,
msg='The suggested renewal interval provided by ARI has begun{0}'.format(msg_append),
supports_ari=True,
)
complete(True, msg='The suggested renewal interval provided by ARI has begun{0}'.format(msg_append))
else:
random_time = backend.interpolate_timestamp(window_start, window_end, random.random())
if now > random_time:
module.exit_json(
changed=False,
should_renew=True,
msg='The picked random renewal time {0} in sugested renewal internal provided by ARI is in the past{1}'.format(random_time, msg_append),
supports_ari=True,
complete(
True,
msg='The picked random renewal time {0} in sugested renewal internal provided by ARI is in the past{1}'.format(
random_time,
msg_append,
),
)
# TODO check remaining_days
if module.params['remaining_days'] is not None:
remaining_days = (cert_info.not_valid_after - now).days
if remaining_days < module.params['remaining_days']:
module.exit_json(
changed=False,
should_renew=True,
msg='The certificate expires in {0} days'.format(remaining_days),
supports_ari=False,
)
complete(True, msg='The certificate expires in {0} days'.format(remaining_days))
# TODO check remaining_percentage
if module.params['remaining_percentage'] is not None:
timestamp = backend.interpolate_timestamp(cert_info.not_valid_before, cert_info.not_valid_after, 1 - module.params['remaining_percentage'])
if timestamp < now:
module.exit_json(
changed=False,
should_renew=True,
complete(
True,
msg="The remaining percentage {0}% of the certificate's lifespan was reached on {1}".format(
module.params['remaining_percentage'] * 100,
timestamp,
),
supports_ari=False,
)
module.exit_json(
changed=False,
should_renew=False,
msg=no_renewal_msg,
supports_ari=renewal_ari,
)
complete(False)
except ModuleFailException as e:
e.do_fail(module)

View File

@ -38,6 +38,9 @@
terms_agreed: true
account_email: "example@example.org"
## OBTAIN CERTIFICATE INFOS ###################################################################
- name: Dump OpenSSL x509 info
command:
cmd: openssl x509 -in {{ remote_tmp_dir }}/cert-1.pem -noout -text
- name: Obtain certificate information
x509_certificate_info:
path: "{{ remote_tmp_dir }}/cert-1.pem"
@ -46,7 +49,7 @@
slurp:
src: '{{ remote_tmp_dir }}/cert-1.pem'
register: slurp_cert_1
- name: Obtain certificate information (1/6)
- name: Obtain certificate information (1/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
@ -55,7 +58,7 @@
validate_certs: false
# Certificate is valid for ~1826 days
register: cert_1_renewal_1
- name: Obtain certificate information (2/6)
- name: Obtain certificate information (2/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
@ -66,7 +69,7 @@
remaining_days: 1000
remaining_percentage: 0.5
register: cert_1_renewal_2
- name: Obtain certificate information (3/6)
- name: Obtain certificate information (3/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_content: "{{ slurp_cert_1.content | b64decode }}"
@ -76,7 +79,7 @@
now: +1800d
# Certificate is valid for ~26 days
register: cert_1_renewal_3
- name: Obtain certificate information (4/6)
- name: Obtain certificate information (4/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
@ -88,7 +91,7 @@
remaining_days: 30
remaining_percentage: 0.1
register: cert_1_renewal_4
- name: Obtain certificate information (5/6)
- name: Obtain certificate information (5/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
@ -100,7 +103,7 @@
remaining_days: 30
remaining_percentage: 0.01
register: cert_1_renewal_5
- name: Obtain certificate information (6/6)
- name: Obtain certificate information (6/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
@ -112,3 +115,31 @@
remaining_days: 10
remaining_percentage: 0.03
register: cert_1_renewal_6
- name: Obtain certificate information (7/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-1.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1830d
# Certificate is no longer valid
register: cert_1_renewal_7
- name: Obtain certificate information (8/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
now: +1830d
# Certificate is no longer valid
register: cert_1_renewal_8
- name: Obtain certificate information (9/9)
acme_certificate_renewal_info:
select_crypto_backend: "{{ select_crypto_backend }}"
certificate_path: "{{ remote_tmp_dir }}/cert-does-not-exist.pem"
acme_version: 2
acme_directory: https://{{ acme_host }}:14000/dir
validate_certs: false
# Certificate is no longer valid
register: cert_1_renewal_9

View File

@ -9,20 +9,39 @@
- cert_1_renewal_1.should_renew == false
- cert_1_renewal_1.msg == 'The certificate is still valid and no condition was reached'
- cert_1_renewal_1.supports_ari == supports_ari
- cert_1_renewal_1.cert_id is string or not can_have_cert_id
- cert_1_renewal_2.should_renew == false
- cert_1_renewal_2.msg == 'The certificate is still valid and no condition was reached'
- cert_1_renewal_2.supports_ari == supports_ari
- cert_1_renewal_2.cert_id is string or not can_have_cert_id
- cert_1_renewal_3.should_renew == false
- cert_1_renewal_3.msg == 'The certificate is still valid and no condition was reached'
- cert_1_renewal_3.supports_ari == supports_ari
- cert_1_renewal_3.cert_id is string or not can_have_cert_id
- cert_1_renewal_4.should_renew == true
- cert_1_renewal_4.msg == 'The certificate expires in 25 days'
- cert_1_renewal_4.supports_ari == supports_ari
- cert_1_renewal_4.cert_id is string or not can_have_cert_id
- cert_1_renewal_5.should_renew == true
- cert_1_renewal_5.msg == 'The certificate expires in 25 days'
- cert_1_renewal_5.supports_ari == supports_ari
- cert_1_renewal_5.cert_id is string or not can_have_cert_id
- cert_1_renewal_6.should_renew == true
- cert_1_renewal_6.msg.startswith("The remaining percentage 3.0% of the certificate's lifespan was reached on ")
- cert_1_renewal_6.supports_ari == supports_ari
- cert_1_renewal_6.cert_id is string or not can_have_cert_id
- cert_1_renewal_7.should_renew == true
- cert_1_renewal_7.msg == 'The certificate has already expired'
- cert_1_renewal_7.supports_ari == false
- cert_1_renewal_7.cert_id is string or not can_have_cert_id
- cert_1_renewal_8.should_renew == true
- cert_1_renewal_8.msg == 'No certificate was specified'
- cert_1_renewal_8.supports_ari == false
- cert_1_renewal_8.cert_id is not defined
- cert_1_renewal_9.should_renew == true
- cert_1_renewal_9.msg == 'The certificate file does not exist'
- cert_1_renewal_9.supports_ari == false
- cert_1_renewal_9.cert_id is not defined
vars:
can_have_cert_id: cert_1_info.authority_key_identifier is string
supports_ari: false

View File

@ -81,9 +81,12 @@ TEST_CSRS = [
TEST_CERT = load_fixture("cert_1.pem")
TEST_CERT_2 = load_fixture("cert_2.pem")
TEST_CERT_OPENSSL_OUTPUT = load_fixture("cert_1.txt")
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 = [
@ -93,18 +96,28 @@ TEST_CERT_DAYS = [
]
TEST_CERT_INFO = 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 = [
(
TEST_CERT,
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_OPENSSL_OUTPUT,
),
(TEST_CERT, TEST_CERT_INFO, 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),
]

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