def main(): argument_spec = get_default_argspec() argument_spec.update( dict( url=dict(type='str'), method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'), content=dict(type='str'), fail_on_acme_error=dict(type='bool', default=True), )) module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=(['account_key_src', 'account_key_content'], ), required_if=( ['method', 'get', ['url']], ['method', 'post', ['url', 'content']], [ 'method', 'get', ['account_key_src', 'account_key_content'], True ], [ 'method', 'post', ['account_key_src', 'account_key_content'], True ], ), ) backend = create_backend(module, False) result = dict() changed = False try: # Get hold of ACMEClient and ACMEAccount objects (includes directory) client = ACMEClient(module, backend) method = module.params['method'] result['directory'] = client.directory.directory # Do we have to do more requests? if method != 'directory-only': url = module.params['url'] fail_on_acme_error = module.params['fail_on_acme_error'] # Do request if method == 'get': data, info = client.get_request(url, parse_json_result=False, fail_on_error=False) elif method == 'post': changed = True # only POSTs can change data, info = client.send_signed_request( url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False, fail_on_error=False) # Update results result.update(dict( headers=info, output_text=to_native(data), )) # See if we can parse the result as JSON try: result['output_json'] = module.from_json(to_text(data)) except Exception as dummy: pass # Fail if error was returned if fail_on_acme_error and info['status'] >= 400: raise ACMEProtocolException(info=info, content_json=result) # Done! module.exit_json(changed=changed, **result) except ModuleFailException as e: e.do_fail(module, **result)
def main(): argument_spec = get_default_argspec() argument_spec.update( dict(retrieve_orders=dict( type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']), )) module = AnsibleModule( argument_spec=argument_spec, required_one_of=(['account_key_src', 'account_key_content'], ), mutually_exclusive=(['account_key_src', 'account_key_content'], ), supports_check_mode=True, ) if module._name in ('acme_account_facts', 'community.crypto.acme_account_facts'): module.deprecate( "The 'acme_account_facts' module has been renamed to 'acme_account_info'", version='2.0.0', collection_name='community.crypto') backend = create_backend(module, True) try: client = ACMEClient(module, backend) account = ACMEAccount(client) # Check whether account exists created, account_data = account.setup_account( [], allow_creation=False, remove_account_uri_if_not_exists=True, ) if created: raise AssertionError('Unwanted account creation') result = { 'changed': False, 'exists': client.account_uri is not None, 'account_uri': client.account_uri, } if client.account_uri is not None: # Make sure promised data is there if 'contact' not in account_data: account_data['contact'] = [] account_data['public_account_key'] = client.account_key_data['jwk'] result['account'] = account_data # Retrieve orders list if account_data.get( 'orders') and module.params['retrieve_orders'] != 'ignore': orders = get_orders_list(module, client, account_data['orders']) result['order_uris'] = orders if module.params['retrieve_orders'] == 'url_list': module.deprecate( 'retrieve_orders=url_list now returns the order URI list as `order_uris`.' ' Right now it also returns this list as `orders` for backwards compatibility,' ' but this will stop in community.crypto 2.0.0', version='2.0.0', collection_name='community.crypto') result['orders'] = orders if module.params['retrieve_orders'] == 'object_list': result['orders'] = [ get_order(client, order) for order in orders ] module.exit_json(**result) except ModuleFailException as e: e.do_fail(module)
def main(): argument_spec = get_default_argspec() argument_spec.update( dict( private_key_src=dict(type='path'), private_key_content=dict(type='str', no_log=True), private_key_passphrase=dict(type='str', no_log=True), certificate=dict(type='path', required=True), revoke_reason=dict(type='int'), )) module = AnsibleModule( argument_spec=argument_spec, required_one_of=([ 'account_key_src', 'account_key_content', 'private_key_src', 'private_key_content' ], ), mutually_exclusive=([ 'account_key_src', 'account_key_content', 'private_key_src', 'private_key_content' ], ), supports_check_mode=False, ) backend = create_backend(module, False) try: client = ACMEClient(module, backend) account = ACMEAccount(client) # Load certificate certificate = pem_to_der(module.params.get('certificate')) certificate = nopad_b64(certificate) # Construct payload payload = {'certificate': certificate} if module.params.get('revoke_reason') is not None: payload['reason'] = module.params.get('revoke_reason') # Determine endpoint if module.params.get('acme_version') == 1: endpoint = client.directory['revoke-cert'] payload['resource'] = 'revoke-cert' else: endpoint = client.directory['revokeCert'] # Get hold of private key (if available) and make sure it comes from disk private_key = module.params.get('private_key_src') private_key_content = module.params.get('private_key_content') # Revoke certificate if private_key or private_key_content: passphrase = module.params['private_key_passphrase'] # Step 1: load and parse private key try: private_key_data = client.parse_key(private_key, private_key_content, passphrase=passphrase) except KeyParsingError as e: raise ModuleFailException( "Error while parsing private key: {msg}".format(msg=e.msg)) # Step 2: sign revokation request with private key jws_header = { "alg": private_key_data['alg'], "jwk": private_key_data['jwk'], } result, info = client.send_signed_request( endpoint, payload, key_data=private_key_data, jws_header=jws_header, fail_on_error=False) else: # Step 1: get hold of account URI created, account_data = account.setup_account(allow_creation=False) if created: raise AssertionError('Unwanted account creation') if account_data is None: raise ModuleFailException( msg='Account does not exist or is deactivated.') # Step 2: sign revokation request with account key result, info = client.send_signed_request(endpoint, payload, fail_on_error=False) if info['status'] != 200: already_revoked = False # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6) if result.get( 'type') == 'urn:ietf:params:acme:error:alreadyRevoked': already_revoked = True else: # Hack for Boulder errors if module.params.get('acme_version') == 1: error_type = 'urn:acme:error:malformed' else: error_type = 'urn:ietf:params:acme:error:malformed' if result.get('type') == error_type and result.get( 'detail') == 'Certificate already revoked': # Fallback: boulder returns this in case the certificate was already revoked. already_revoked = True # If we know the certificate was already revoked, we don't fail, # but successfully terminate while indicating no change if already_revoked: module.exit_json(changed=False) raise ACMEProtocolException('Failed to revoke certificate', info=info, content_json=result) module.exit_json(changed=True) except ModuleFailException as e: e.do_fail(module)
def main(): argument_spec = get_default_argspec() argument_spec.update( dict( modify_account=dict(type='bool', default=True), account_email=dict(type='str'), agreement=dict(type='str'), terms_agreed=dict(type='bool', default=False), challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01']), csr=dict(type='path', aliases=['src']), csr_content=dict(type='str'), data=dict(type='dict'), dest=dict(type='path', aliases=['cert']), fullchain_dest=dict(type='path', aliases=['fullchain']), chain_dest=dict(type='path', aliases=['chain']), remaining_days=dict(type='int', default=10), deactivate_authzs=dict(type='bool', default=False), force=dict(type='bool', default=False), retrieve_all_alternates=dict(type='bool', default=False), select_chain=dict(type='list', elements='dict', options=dict( test_certificates=dict( type='str', default='all', choices=['first', 'last', 'all']), issuer=dict(type='dict'), subject=dict(type='dict'), subject_key_identifier=dict(type='str'), authority_key_identifier=dict(type='str'), )), )) module = AnsibleModule( argument_spec=argument_spec, required_one_of=( ['account_key_src', 'account_key_content'], ['dest', 'fullchain_dest'], ['csr', 'csr_content'], ), mutually_exclusive=( ['account_key_src', 'account_key_content'], ['csr', 'csr_content'], ), supports_check_mode=True, ) backend = create_backend(module, False) try: if module.params.get('dest'): cert_days = backend.get_cert_days(module.params['dest']) else: cert_days = backend.get_cert_days(module.params['fullchain_dest']) if module.params[ 'force'] or cert_days < module.params['remaining_days']: # If checkmode is active, base the changed state solely on the status # of the certificate file as all other actions (accessing an account, checking # the authorization status...) would lead to potential changes of the current # state if module.check_mode: module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days) else: client = ACMECertificateClient(module, backend) client.cert_days = cert_days other = dict() is_first_step = client.is_first_step() if is_first_step: # First run: start challenges / start new order client.start_challenges() else: # Second run: finish challenges, and get certificate try: client.finish_challenges() client.get_certificate() if client.all_chains is not None: other['all_chains'] = client.all_chains finally: if module.params['deactivate_authzs']: client.deactivate_authzs() data, data_dns = client.get_challenges_data( first_step=is_first_step) auths = dict() for k, v in client.authorizations.items(): # Remove "type:" from key auths[split_identifier(k)[1]] = v.to_json() module.exit_json(changed=client.changed, authorizations=auths, finalize_uri=client.order.finalize_uri if client.order else None, order_uri=client.order_uri, account_uri=client.client.account_uri, challenge_data=data, challenge_data_dns=data_dns, cert_days=client.cert_days, **other) else: module.exit_json(changed=False, cert_days=cert_days) except ModuleFailException as e: e.do_fail(module)
def main(): argument_spec = get_default_argspec() argument_spec.update(dict( terms_agreed=dict(type='bool', default=False), state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']), allow_creation=dict(type='bool', default=True), contact=dict(type='list', elements='str', default=[]), new_account_key_src=dict(type='path'), new_account_key_content=dict(type='str', no_log=True), new_account_key_passphrase=dict(type='str', no_log=True), external_account_binding=dict(type='dict', options=dict( kid=dict(type='str', required=True), alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']), key=dict(type='str', required=True, no_log=True), )) )) module = AnsibleModule( argument_spec=argument_spec, required_one_of=( ['account_key_src', 'account_key_content'], ), mutually_exclusive=( ['account_key_src', 'account_key_content'], ['new_account_key_src', 'new_account_key_content'], ), required_if=( # Make sure that for state == changed_key, one of # new_account_key_src and new_account_key_content are specified ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True], ), supports_check_mode=True, ) backend = create_backend(module, True) if module.params['external_account_binding']: # Make sure padding is there key = module.params['external_account_binding']['key'] if len(key) % 4 != 0: key = key + ('=' * (4 - (len(key) % 4))) # Make sure key is Base64 encoded try: base64.urlsafe_b64decode(key) except Exception as e: module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e) module.params['external_account_binding']['key'] = key try: client = ACMEClient(module, backend) account = ACMEAccount(client) changed = False state = module.params.get('state') diff_before = {} diff_after = {} if state == 'absent': created, account_data = account.setup_account(allow_creation=False) if account_data: diff_before = dict(account_data) diff_before['public_account_key'] = client.account_key_data['jwk'] if created: raise AssertionError('Unwanted account creation') if account_data is not None: # Account is not yet deactivated if not module.check_mode: # Deactivate it payload = { 'status': 'deactivated' } result, info = client.send_signed_request( client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200]) changed = True elif state == 'present': allow_creation = module.params.get('allow_creation') contact = [str(v) for v in module.params.get('contact')] terms_agreed = module.params.get('terms_agreed') external_account_binding = module.params.get('external_account_binding') created, account_data = account.setup_account( contact, terms_agreed=terms_agreed, allow_creation=allow_creation, external_account_binding=external_account_binding, ) if account_data is None: raise ModuleFailException(msg='Account does not exist or is deactivated.') if created: diff_before = {} else: diff_before = dict(account_data) diff_before['public_account_key'] = client.account_key_data['jwk'] updated = False if not created: updated, account_data = account.update_account(account_data, contact) changed = created or updated diff_after = dict(account_data) diff_after['public_account_key'] = client.account_key_data['jwk'] elif state == 'changed_key': # Parse new account key try: new_key_data = client.parse_key( module.params.get('new_account_key_src'), module.params.get('new_account_key_content'), passphrase=module.params.get('new_account_key_passphrase'), ) except KeyParsingError as e: raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg)) # Verify that the account exists and has not been deactivated created, account_data = account.setup_account(allow_creation=False) if created: raise AssertionError('Unwanted account creation') if account_data is None: raise ModuleFailException(msg='Account does not exist or is deactivated.') diff_before = dict(account_data) diff_before['public_account_key'] = client.account_key_data['jwk'] # Now we can start the account key rollover if not module.check_mode: # Compose inner signed message # https://tools.ietf.org/html/rfc8555#section-7.3.5 url = client.directory['keyChange'] protected = { "alg": new_key_data['alg'], "jwk": new_key_data['jwk'], "url": url, } payload = { "account": client.account_uri, "newKey": new_key_data['jwk'], # specified in draft 12 and older "oldKey": client.account_jwk, # specified in draft 13 and newer } data = client.sign_request(protected, payload, new_key_data) # Send request and verify result result, info = client.send_signed_request( url, data, error_msg='Failed to rollover account key', expected_status_codes=[200]) if module._diff: client.account_key_data = new_key_data client.account_jws_header['alg'] = new_key_data['alg'] diff_after = account.get_account_data() elif module._diff: # Kind of fake diff_after diff_after = dict(diff_before) diff_after['public_account_key'] = new_key_data['jwk'] changed = True result = { 'changed': changed, 'account_uri': client.account_uri, } if module._diff: result['diff'] = { 'before': diff_before, 'after': diff_after, } module.exit_json(**result) except ModuleFailException as e: e.do_fail(module)