acme_certificate: allow to request renewal of a certificate according to ARI (#739)

* Allow to request renewal of a certificate according to ARI in acme_certificate.

* Improve docs.

* Fix typo and use right object.

* Add warning.
pull/742/head^2
Felix Fontein 2024-04-30 10:47:49 +02:00 committed by GitHub
parent 6d4fc589ae
commit 33d278ad8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 52 additions and 2 deletions

View File

@ -0,0 +1,2 @@
minor_changes:
- "acme_certificate - add ``include_renewal_cert_id`` option to allow requesting renewal of a specific certificate according to the current ACME Renewal Information specification draft (https://github.com/ansible-collections/community.crypto/pull/739)."

View File

@ -32,6 +32,7 @@ class Order(object):
self.identifiers = []
for identifier in data['identifiers']:
self.identifiers.append((identifier['type'], identifier['value']))
self.replaces_cert_id = data.get('replaces')
self.finalize_uri = data.get('finalize')
self.certificate_uri = data.get('certificate')
self.authorization_uris = data['authorizations']
@ -44,6 +45,7 @@ class Order(object):
self.status = None
self.identifiers = []
self.replaces_cert_id = None
self.finalize_uri = None
self.certificate_uri = None
self.authorization_uris = []
@ -62,7 +64,7 @@ class Order(object):
return result
@classmethod
def create(cls, client, identifiers):
def create(cls, client, identifiers, replaces_cert_id=None):
'''
Start a new certificate order (ACME v2 protocol).
https://tools.ietf.org/html/rfc8555#section-7.4
@ -76,6 +78,8 @@ class Order(object):
new_order = {
"identifiers": acme_identifiers
}
if replaces_cert_id is not None:
new_order["replaces"] = replaces_cert_id
result, info = client.send_signed_request(
client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201])
return cls.from_json(client, result, info['location'])

View File

@ -293,6 +293,30 @@ options:
- "The identifier must be of the form
V(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)."
type: str
include_renewal_cert_id:
description:
- Determines whether to request renewal of an existing certificate according to
L(the ACME ARI draft 3, https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#section-5).
- This is only used when the certificate specified in O(dest) or O(fullchain_dest) already exists.
- V(never) never sends the certificate ID of the certificate to renew. V(always) will always send it.
- V(when_ari_supported) only sends the certificate ID if the ARI endpoint is found in the ACME directory.
- Generally you should use V(when_ari_supported) if you know that the ACME service supports a compatible
draft (or final version, once it is out) of the ARI extension. V(always) should never be necessary.
If you are not sure, or if you receive strange errors on invalid C(replaces) values in order objects,
use V(never), which also happens to be the default.
- ACME servers might refuse to create new orders with C(replaces) for certificates that already have an
existing order. This can happen if this module is used to create an order, and then the playbook/role
fails in case the challenges cannot be set up. If the playbook/role does not record the order data to
continue with the existing order, but tries to create a new one on the next run, creating the new order
might fail. For this reason, this option should only be set to a value different from V(never) if the
role/playbook using it keeps track of order data accross restarts.
type: str
choices:
- never
- when_ari_supported
- always
default: never
version_added: 2.20.0
'''
EXAMPLES = r'''
@ -586,6 +610,7 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.orders impor
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
compute_cert_id,
pem_to_der,
)
@ -622,6 +647,7 @@ class ACMECertificateClient(object):
self.order_uri = self.data.get('order_uri') if self.data else None
self.all_chains = None
self.select_chain_matcher = []
self.include_renewal_cert_id = module.params['include_renewal_cert_id']
if self.module.params['select_chain']:
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
@ -679,6 +705,15 @@ class ACMECertificateClient(object):
# stored in self.order_uri by the constructor).
return self.order_uri is None
def _get_cert_info_or_none(self):
if self.module.params.get('dest'):
filename = self.module.params['dest']
else:
filename = self.module.params['fullchain_dest']
if not os.path.exists(filename):
return None
return self.client.backend.get_cert_information(cert_filename=filename)
def start_challenges(self):
'''
Create new authorizations for all identifiers of the CSR,
@ -693,7 +728,15 @@ class ACMECertificateClient(object):
authz = Authorization.create(self.client, identifier_type, identifier)
self.authorizations[authz.combined_identifier] = authz
else:
self.order = Order.create(self.client, self.identifiers)
replaces_cert_id = None
if (
self.include_renewal_cert_id == 'always' or
(self.include_renewal_cert_id == 'when_ari_supported' and self.client.directory.has_renewal_info_endpoint())
):
cert_info = self._get_cert_info_or_none()
if cert_info is not None:
replaces_cert_id = compute_cert_id(self.client.backend, cert_info=cert_info)
self.order = Order.create(self.client, self.identifiers, replaces_cert_id)
self.order_uri = self.order.url
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
@ -879,6 +922,7 @@ def main():
subject_key_identifier=dict(type='str'),
authority_key_identifier=dict(type='str'),
)),
include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'),
))
module = AnsibleModule(
argument_spec=argument_spec,