class EcsCertificate(object): ''' Entrust Certificate Services certificate class. ''' def __init__(self, module): self.path = module.params['path'] self.full_chain_path = module.params['full_chain_path'] self.force = module.params['force'] self.backup = module.params['backup'] self.request_type = module.params['request_type'] self.csr = module.params['csr'] # All return values self.changed = False self.filename = None self.tracking_id = None self.cert_status = None self.serial_number = None self.cert_days = None self.cert_details = None self.backup_file = None self.backup_full_chain_file = None self.cert = None self.ecs_client = None if self.path and os.path.exists(self.path): try: self.cert = crypto_utils.load_certificate(self.path, backend='cryptography') except Exception as dummy: self.cert = None # Instantiate the ECS client and then try a no-op connection to verify credentials are valid try: self.ecs_client = ECSClient( entrust_api_user=module.params['entrust_api_user'], entrust_api_key=module.params['entrust_api_key'], entrust_api_cert=module.params['entrust_api_client_cert_path'], entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'], entrust_api_specification_path=module.params['entrust_api_specification_path'] ) except SessionConfigurationException as e: module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e))) try: self.ecs_client.GetAppVersion() except RestOperationException as e: module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message))) # Conversion of the fields that go into the 'tracking' parameter of the request object def convert_tracking_params(self, module): body = {} tracking = {} if module.params['requester_name']: tracking['requesterName'] = module.params['requester_name'] if module.params['requester_email']: tracking['requesterEmail'] = module.params['requester_email'] if module.params['requester_phone']: tracking['requesterPhone'] = module.params['requester_phone'] if module.params['tracking_info']: tracking['trackingInfo'] = module.params['tracking_info'] if module.params['custom_fields']: # Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null' # The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth. custom_fields = {} for k, v in module.params['custom_fields'].items(): if v is not None: custom_fields[k] = v tracking['customFields'] = custom_fields if module.params['additional_emails']: tracking['additionalEmails'] = module.params['additional_emails'] body['tracking'] = tracking return body def convert_cert_subject_params(self, module): body = {} if module.params['subject_alt_name']: body['subjectAltName'] = module.params['subject_alt_name'] if module.params['org']: body['org'] = module.params['org'] if module.params['ou']: body['ou'] = module.params['ou'] return body def convert_general_params(self, module): body = {} if module.params['eku']: body['eku'] = module.params['eku'] if self.request_type == 'new': body['certType'] = module.params['cert_type'] body['clientId'] = module.params['client_id'] body.update(convert_module_param_to_json_bool(module, 'ctLog', 'ct_log')) body.update(convert_module_param_to_json_bool(module, 'endUserKeyStorageAgreement', 'end_user_key_storage_agreement')) return body def convert_expiry_params(self, module): body = {} if module.params['cert_lifetime']: body['certLifetime'] = module.params['cert_lifetime'] elif module.params['cert_expiry']: body['certExpiryDate'] = module.params['cert_expiry'] # If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days elif self.request_type != 'reissue': gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) expiry = gmt_now + datetime.timedelta(days=365) body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") return body def set_tracking_id_by_serial_number(self, module): try: # Use serial_number to identify if certificate is an Entrust Certificate # with an associated tracking ID serial_number = "{0:X}".format(self.cert.serial_number) cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) if len(cert_results) == 1: self.tracking_id = cert_results[0].get('trackingId') except RestOperationException as dummy: # If we fail to find a cert by serial number, that's fine, we just don't set self.tracking_id return def set_cert_details(self, module): try: self.cert_details = self.ecs_client.GetCertificate(trackingId=self.tracking_id) self.cert_status = self.cert_details.get('status') self.serial_number = self.cert_details.get('serialNumber') self.cert_days = calculate_cert_days(self.cert_details.get('expiresAfter')) except RestOperationException as e: module.fail_json('Failed to get details of certificate with tracking_id="{0}", Error: '.format(self.tracking_id), to_native(e.message)) def check(self, module): if self.cert: # We will only treat a certificate as valid if it is found as a managed entrust cert. # We will only set updated tracking ID based on certificate in "path" if it is managed by entrust. self.set_tracking_id_by_serial_number(module) if module.params['tracking_id'] and self.tracking_id and module.params['tracking_id'] != self.tracking_id: module.warn('tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with ' 'tracking_id of "{2}".'.format(module.params['tracking_id'], self.path, self.tracking_id)) # If we did not end up setting tracking_id based on existing cert, get from module params if not self.tracking_id: self.tracking_id = module.params['tracking_id'] if not self.tracking_id: return False self.set_cert_details(module) if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED': return False if self.cert_days < module.params['remaining_days']: return False return True def request_cert(self, module): if not self.check(module) or self.force: body = {} # Read the CSR contents if self.csr and os.path.exists(self.csr): with open(self.csr, 'r') as csr_file: body['csr'] = csr_file.read() # Check if the path is already a cert # tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null # We will be performing a reissue operation. if self.request_type != 'new' and not self.tracking_id: module.warn('No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task' 'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}' .format(self.path, self.path, self.request_type)) self.request_type = 'new' elif self.request_type == 'new' and self.tracking_id: module.warn('Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a' 'reissue or renew') # Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid # existing certificate is found, do not need warnings. body.update(self.convert_tracking_params(module)) body.update(self.convert_cert_subject_params(module)) body.update(self.convert_general_params(module)) body.update(self.convert_expiry_params(module)) if not module.check_mode: try: if self.request_type == 'validate_only': body['validateOnly'] = 'true' result = self.ecs_client.NewCertRequest(Body=body) if self.request_type == 'new': result = self.ecs_client.NewCertRequest(Body=body) elif self.request_type == 'renew': result = self.ecs_client.RenewCertRequest(trackingId=self.tracking_id, Body=body) elif self.request_type == 'reissue': result = self.ecs_client.ReissueCertRequest(trackingId=self.tracking_id, Body=body) self.tracking_id = result.get('trackingId') self.set_cert_details(module) except RestOperationException as e: module.fail_json(msg='Failed to request new certificate from Entrust (ECS) {0}'.format(e.message)) if self.request_type != 'validate_only': if self.backup: self.backup_file = module.backup_local(self.path) crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) if self.full_chain_path and self.cert_details.get('chainCerts'): if self.backup: self.backup_full_chain_file = module.backup_local(self.full_chain_path) chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) self.changed = True # If there is no certificate present in path but a tracking ID was specified, save it to disk elif not os.path.exists(self.path) and self.tracking_id: if not module.check_mode: crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) if self.full_chain_path and self.cert_details.get('chainCerts'): chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) self.changed = True def dump(self): result = { 'changed': self.changed, 'filename': self.path, 'tracking_id': self.tracking_id, 'cert_status': self.cert_status, 'serial_number': self.serial_number, 'cert_days': self.cert_days, 'cert_details': self.cert_details, } if self.backup_file: result['backup_file'] = self.backup_file result['backup_full_chain_file'] = self.backup_full_chain_file return result
class EcsDomain(object): ''' Entrust Certificate Services domain class. ''' def __init__(self, module): self.changed = False self.domain_status = None self.verification_method = None self.file_location = None self.file_contents = None self.dns_location = None self.dns_contents = None self.dns_resource_type = None self.emails = None self.ov_eligible = None self.ov_days_remaining = None self.ev_eligble = None self.ev_days_remaining = None # Note that verification_method is the 'current' verification # method of the domain, we'll use module.params when requesting a new # one, in case the verification method has changed. self.verification_method = None self.ecs_client = None # Instantiate the ECS client and then try a no-op connection to verify credentials are valid try: self.ecs_client = ECSClient( entrust_api_user=module.params['entrust_api_user'], entrust_api_key=module.params['entrust_api_key'], entrust_api_cert=module.params['entrust_api_client_cert_path'], entrust_api_cert_key=module. params['entrust_api_client_cert_key_path'], entrust_api_specification_path=module. params['entrust_api_specification_path']) except SessionConfigurationException as e: module.fail_json( msg='Failed to initialize Entrust Provider: {0}'.format( to_native(e))) try: self.ecs_client.GetAppVersion() except RestOperationException as e: module.fail_json( msg= 'Please verify credential information. Received exception when testing ECS connection: {0}' .format(to_native(e.message))) def set_domain_details(self, domain_details): if domain_details.get('verificationMethod'): self.verification_method = domain_details[ 'verificationMethod'].lower() self.domain_status = domain_details['verificationStatus'] self.ov_eligible = domain_details.get('ovEligible') self.ov_days_remaining = calculate_days_remaining( domain_details.get('ovExpiry')) self.ev_eligible = domain_details.get('evEligible') self.ev_days_remaining = calculate_days_remaining( domain_details.get('evExpiry')) self.client_id = domain_details['clientId'] if self.verification_method == 'dns' and domain_details.get( 'dnsMethod'): self.dns_location = domain_details['dnsMethod']['recordDomain'] self.dns_resource_type = domain_details['dnsMethod']['recordType'] self.dns_contents = domain_details['dnsMethod']['recordValue'] elif self.verification_method == 'web_server' and domain_details.get( 'webServerMethod'): self.file_location = domain_details['webServerMethod'][ 'fileLocation'] self.file_contents = domain_details['webServerMethod'][ 'fileContents'] elif self.verification_method == 'email' and domain_details.get( 'emailMethod'): self.emails = domain_details['emailMethod'] def check(self, module): try: domain_details = self.ecs_client.GetDomain( clientId=module.params['client_id'], domain=module.params['domain_name']) self.set_domain_details(domain_details) if self.domain_status != 'APPROVED' and self.domain_status != 'INITIAL_VERIFICATION' and self.domain_status != 'RE_VERIFICATION': return False # If domain verification is in process, we want to return the random values and treat it as a valid. if self.domain_status == 'INITIAL_VERIFICATION' or self.domain_status == 'RE_VERIFICATION': # Unless the verification method has changed, in which case we need to do a reverify request. if self.verification_method != module.params[ 'verification_method']: return False if self.domain_status == 'EXPIRING': return False return True except RestOperationException as dummy: return False def request_domain(self, module): if not self.check(module): body = {} body['verificationMethod'] = module.params[ 'verification_method'].upper() if module.params['verification_method'] == 'email': emailMethod = {} if module.params['verification_email']: emailMethod['emailSource'] = 'SPECIFIED' emailMethod['email'] = module.params['verification_email'] else: emailMethod['emailSource'] = 'INCLUDE_WHOIS' body['emailMethod'] = emailMethod # Only populate domain name in body if it is not an existing domain if not self.domain_status: body['domainName'] = module.params['domain_name'] try: if not self.domain_status: self.ecs_client.AddDomain( clientId=module.params['client_id'], Body=body) else: self.ecs_client.ReverifyDomain( clientId=module.params['client_id'], domain=module.params['domain_name'], Body=body) time.sleep(5) result = self.ecs_client.GetDomain( clientId=module.params['client_id'], domain=module.params['domain_name']) # It takes a bit of time before the random values are available if module.params[ 'verification_method'] == 'dns' or module.params[ 'verification_method'] == 'web_server': for i in range(4): # Check both that random values are now available, and that they're different than were populated by previous 'check' if module.params['verification_method'] == 'dns': if result.get('dnsMethod') and result['dnsMethod'][ 'recordValue'] != self.dns_contents: break elif module.params[ 'verification_method'] == 'web_server': if result.get('webServerMethod') and result[ 'webServerMethod'][ 'fileContents'] != self.file_contents: break time.sleep(10) result = self.ecs_client.GetDomain( clientId=module.params['client_id'], domain=module.params['domain_name']) self.changed = True self.set_domain_details(result) except RestOperationException as e: module.fail_json( msg= 'Failed to request domain validation from Entrust (ECS) {0}' .format(e.message)) def dump(self): result = { 'changed': self.changed, 'client_id': self.client_id, 'domain_status': self.domain_status, } if self.verification_method: result['verification_method'] = self.verification_method if self.ov_eligible is not None: result['ov_eligible'] = self.ov_eligible if self.ov_days_remaining: result['ov_days_remaining'] = self.ov_days_remaining if self.ev_eligible is not None: result['ev_eligible'] = self.ev_eligible if self.ev_days_remaining: result['ev_days_remaining'] = self.ev_days_remaining if self.emails: result['emails'] = self.emails if self.verification_method == 'dns': result['dns_location'] = self.dns_location result['dns_contents'] = self.dns_contents result['dns_resource_type'] = self.dns_resource_type elif self.verification_method == 'web_server': result['file_location'] = self.file_location result['file_contents'] = self.file_contents elif self.verification_method == 'email': result['emails'] = self.emails return result