diff --git a/changelogs/fragments/739-acme_certificate-include_renewal_cert_id.yml b/changelogs/fragments/739-acme_certificate-include_renewal_cert_id.yml new file mode 100644 index 000000000..9c04778ce --- /dev/null +++ b/changelogs/fragments/739-acme_certificate-include_renewal_cert_id.yml @@ -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)." diff --git a/plugins/module_utils/acme/orders.py b/plugins/module_utils/acme/orders.py index 732b430df..98c28445f 100644 --- a/plugins/module_utils/acme/orders.py +++ b/plugins/module_utils/acme/orders.py @@ -32,6 +32,7 @@ def _setup(self, client, data): 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 @@ def __init__(self, url): 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 @@ def from_url(cls, client, url): 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 @@ def create(cls, client, identifiers): 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']) diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py index 909e60a47..846c4cd88 100644 --- a/plugins/modules/acme_certificate.py +++ b/plugins/modules/acme_certificate.py @@ -293,6 +293,20 @@ - "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 if 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. + type: str + choices: + - never + - when_ari_supported + - always + default: never + version_added: 2.20.0 ''' EXAMPLES = r''' @@ -586,6 +600,7 @@ ) from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ( + compute_cert_id, pem_to_der, ) @@ -622,6 +637,7 @@ def __init__(self, module, backend): 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 +695,15 @@ def is_first_step(self): # stored in self.order_uri by the constructor). return self.order_uri is None + def _get_cert_info_or_none(self): + if module.params.get('dest'): + filename = self.module.params['dest'] + else: + filename = self.module.params['fullchain_dest'] + if not os.path.exist(filename): + return None + return self.backend.get_cert_information(cert_filename=filename) + def start_challenges(self): ''' Create new authorizations for all identifiers of the CSR, @@ -693,7 +718,15 @@ def start_challenges(self): 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.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 +912,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,