Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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