Entrust Datacard - Support for "entrust" provider in openssl_certificate module (#59272)

* Addition of entrust provider to openssl_certificate module

* Fix native return values of error messages and JSON response.

* Documentation and syntax fixes per ansibot.

* Refactored structure of for loop due to ansible test failures in python 2.6

* Remove OCSP functionality for inclusion in possible seperate future pull request.

* Remove reissue support.

* Indicate the entrust parameters are specific to entrust.

* Comment fixes to make it clear module_utils request is used.

* Fixes to not_after documentation

* Response to pull request comments and cleanup of error handling for bad connections to properly use the 'six' HttpError for compatibility with both Python 2/3 underlying url libraries.

* pep8/pycodestyle fixes.

* Added code fragment and response to comments.

* Update license to simplified BSD

* Fixed botmeta typo

* Include license text in api.yml

* Remove unsupported certificate types, and always submit an explicit organization to match organization in CSR

* Fix documentation misquote, add expired to a comment, and fix path check timing.

* Update changelogs/fragments/59272-support-for-entrust-provider-in-openssl_certificate_module.yaml

Co-Authored-By: Felix Fontein <felix@fontein.de>
This commit is contained in:
Chris Trufan 2019-08-17 15:32:02 -04:00 committed by Felix Fontein
parent ab07c206aa
commit 86366530e8
5 changed files with 655 additions and 4 deletions

3
.github/BOTMETA.yml vendored
View file

@ -216,6 +216,8 @@ files:
labels: crypto labels: crypto
maintainers: $team_crypto maintainers: $team_crypto
supershipit: felixfontein supershipit: felixfontein
$modules/crypto/openssl_certificate.py:
maintainers: ctrufan
$modules/database/influxdb/: kamsz $modules/database/influxdb/: kamsz
$modules/database/mssql/mssql_db.py: Jmainguy kenichi-ogawa-1988 $modules/database/mssql/mssql_db.py: Jmainguy kenichi-ogawa-1988
$modules/database/mysql/: &mysql $modules/database/mysql/: &mysql
@ -668,6 +670,7 @@ files:
labels: labels:
- aws - aws
- cloud - cloud
maintainers: ctrufan $team_crypto
$module_utils/facts: $module_utils/facts:
support: core support: core
$module_utils/facts/hardware/aix.py: *aix $module_utils/facts/hardware/aix.py: *aix

View file

@ -0,0 +1,2 @@
minor_changes:
- openssl_certificate - Add support for a new provider ``entrust`` (https://github.com/ansible/ansible/pull/59272).

View file

View file

@ -0,0 +1,351 @@
# -*- coding: utf-8 -*-
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is licensed under the
# Modified BSD License. Modules you write using this snippet, which is embedded
# dynamically by Ansible, still belong to the author of the module, and may assign
# their own license to the complete work.
#
# Copyright (c), Entrust Datacard Corporation, 2019
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
import os
import re
import time
import traceback
from ansible.module_utils._text import to_text, to_native
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils.urls import Request
YAML_IMP_ERR = None
try:
import yaml
except ImportError:
YAML_FOUND = False
YAML_IMP_ERR = traceback.format_exc()
else:
YAML_FOUND = True
valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$")
class SessionConfigurationException(Exception):
""" Raised if we cannot configure a session with the API """
pass
class RestOperationException(Exception):
""" Encapsulate a REST API error """
def __init__(self, error):
self.status = to_native(error.get("status", None))
self.errors = [to_native(err.get("message")) for err in error.get("errors", {})]
self.message = to_native(" ".join(self.errors))
def generate_docstring(operation_spec):
"""Generate a docstring for an operation defined in operation_spec (swagger)"""
# Description of the operation
docs = operation_spec.get("description", "No Description")
docs += "\n\n"
# Parameters of the operation
parameters = operation_spec.get("parameters", [])
if len(parameters) != 0:
docs += "\tArguments:\n\n"
for parameter in parameters:
docs += "{0} ({1}:{2}): {3}\n".format(
parameter.get("name"),
parameter.get("type", "No Type"),
"Required" if parameter.get("required", False) else "Not Required",
parameter.get("description"),
)
return docs
def bind(instance, method, operation_spec):
def binding_scope_fn(*args, **kwargs):
return method(instance, *args, **kwargs)
# Make sure we don't confuse users; add the proper name and documentation to the function.
# Users can use !help(<function>) to get help on the function from interactive python or pdb
operation_name = operation_spec.get("operationId").split("Using")[0]
binding_scope_fn.__name__ = str(operation_name)
binding_scope_fn.__doc__ = generate_docstring(operation_spec)
return binding_scope_fn
class RestOperation(object):
def __init__(self, session, uri, method, parameters=None):
self.session = session
self.method = method
if parameters is None:
self.parameters = {}
else:
self.parameters = parameters
self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri)
def restmethod(self, *args, **kwargs):
"""Do the hard work of making the request here"""
# gather named path parameters and do substitution on the URL
if self.parameters:
path_parameters = {}
body_parameters = {}
query_parameters = {}
for x in self.parameters:
expected_location = x.get("in")
key_name = x.get("name", None)
key_value = kwargs.get(key_name, None)
if expected_location == "path" and key_name and key_value:
path_parameters.update({key_name: key_value})
elif expected_location == "body" and key_name and key_value:
body_parameters.update({key_name: key_value})
elif expected_location == "query" and key_name and key_value:
query_parameters.update({key_name: key_value})
if len(body_parameters.keys()) >= 1:
body_parameters = body_parameters.get(list(body_parameters.keys())[0])
else:
body_parameters = None
else:
path_parameters = {}
query_parameters = {}
body_parameters = None
# This will fail if we have not set path parameters with a KeyError
url = self.url.format(**path_parameters)
if query_parameters:
# modify the URL to add path parameters
url = url + "?" + urlencode(query_parameters)
try:
if body_parameters:
body_parameters_json = json.dumps(body_parameters)
response = self.session.request.open(method=self.method, url=url, data=body_parameters_json)
else:
response = self.session.request.open(method=self.method, url=url)
request_error = False
except HTTPError as e:
# An HTTPError has the same methods available as a valid response from request.open
response = e
request_error = True
# Return the result if JSON and success ({} for empty responses)
# Raise an exception if there was a failure.
try:
result_code = response.getcode()
result = json.loads(response.read())
except ValueError:
result = {}
if result or result == {}:
if result_code and result_code < 400:
return result
else:
raise RestOperationException(result)
# Raise a generic RestOperationException if this fails
raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]})
class Resource(object):
""" Implement basic CRUD operations against a path. """
def __init__(self, session):
self.session = session
self.parameters = {}
for url in session._spec.get("paths").keys():
methods = session._spec.get("paths").get(url)
for method in methods.keys():
operation_spec = methods.get(method)
operation_name = operation_spec.get("operationId", None)
parameters = operation_spec.get("parameters")
if not operation_name:
if method.lower() == "post":
operation_name = "Create"
elif method.lower() == "get":
operation_name = "Get"
elif method.lower() == "put":
operation_name = "Update"
elif method.lower() == "delete":
operation_name = "Delete"
elif method.lower() == "patch":
operation_name = "Patch"
else:
raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method)))
# Get the non-parameter parts of the URL and append to the operation name
# e.g /application/version -> GetApplicationVersion
# e.g. /application/{id} -> GetApplication
# This may lead to duplicates, which we must prevent.
operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "")
operation_spec["operationId"] = operation_name
op = RestOperation(session, url, method, parameters)
setattr(self, operation_name, bind(self, op.restmethod, operation_spec))
# Session to encapsulate the connection parameters of the module_utils Request object, the api spec, etc
class ECSSession(object):
def __init__(self, name, **kwargs):
"""
Initialize our session
"""
self._set_config(name, **kwargs)
def client(self):
resource = Resource(self)
return resource
def _set_config(self, name, **kwargs):
headers = {"Content-Type": "application/json"}
self.request = Request(headers=headers, timeout=60)
configurators = [self._read_config_vars]
for configurator in configurators:
self._config = configurator(name, **kwargs)
if self._config:
break
if self._config is None:
raise SessionConfigurationException(to_native("No Configuration Found."))
# set up auth if passed
entrust_api_user = self.get_config("entrust_api_user")
entrust_api_key = self.get_config("entrust_api_key")
if entrust_api_user and entrust_api_key:
self.request.url_username = entrust_api_user
self.request.url_password = entrust_api_key
else:
raise SessionConfigurationException(to_native("User and key must be provided."))
# set up client certificate if passed (support all-in one or cert + key)
entrust_api_cert = self.get_config("entrust_api_cert")
entrust_api_cert_key = self.get_config("entrust_api_cert_key")
if entrust_api_cert:
self.request.client_cert = entrust_api_cert
if entrust_api_cert_key:
self.request.client_key = entrust_api_cert_key
else:
raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided."))
# set up the spec
entrust_api_specification_path = self.get_config("entrust_api_specification_path")
if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path):
raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path)))
if not valid_file_format.match(entrust_api_specification_path):
raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml"))
self.verify = True
if entrust_api_specification_path.startswith("http"):
try:
http_response = Request().open(method="GET", url=entrust_api_specification_path)
http_response_contents = http_response.read()
if entrust_api_specification_path.endswith(".json"):
self._spec = json.load(http_response_contents)
elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"):
self._spec = yaml.safe_load(http_response_contents)
except HTTPError as e:
raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format(
entrust_api_specification_path, e.getcode())))
else:
with open(entrust_api_specification_path) as f:
if ".json" in entrust_api_specification_path:
self._spec = json.load(f)
elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path:
self._spec = yaml.safe_load(f)
def get_config(self, item):
return self._config.get(item, None)
def _read_config_vars(self, name, **kwargs):
""" Read configuration from variables passed to the module. """
config = {}
entrust_api_specification_path = kwargs.get("entrust_api_specification_path")
if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)):
raise SessionConfigurationException(
to_native(
"Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format(
entrust_api_specification_path
)
)
)
for required_file in ["entrust_api_cert", "entrust_api_cert_key"]:
file_path = kwargs.get(required_file)
if not file_path or not os.path.isfile(file_path):
raise SessionConfigurationException(
to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path))
)
for required_var in ["entrust_api_user", "entrust_api_key"]:
if not kwargs.get(required_var):
raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var)))
config["entrust_api_cert"] = kwargs.get("entrust_api_cert")
config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key")
config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path")
config["entrust_api_user"] = kwargs.get("entrust_api_user")
config["entrust_api_key"] = kwargs.get("entrust_api_key")
return config
def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None):
"""Create an ECS client"""
if not YAML_FOUND:
raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
if entrust_api_specification_path is None:
entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml"
# Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases
entrust_api_user = to_text(entrust_api_user)
entrust_api_key = to_text(entrust_api_key)
entrust_api_cert_key = to_text(entrust_api_cert_key)
entrust_api_specification_path = to_text(entrust_api_specification_path)
return ECSSession(
"ecs",
entrust_api_user=entrust_api_user,
entrust_api_key=entrust_api_key,
entrust_api_cert=entrust_api_cert,
entrust_api_cert_key=entrust_api_cert_key,
entrust_api_specification_path=entrust_api_specification_path,
).client()

View file

@ -19,7 +19,7 @@ version_added: "2.4"
short_description: Generate and/or check OpenSSL certificates short_description: Generate and/or check OpenSSL certificates
description: description:
- This module allows one to (re)generate OpenSSL certificates. - This module allows one to (re)generate OpenSSL certificates.
- It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly)) - It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly), C(entrust))
for your certificate. for your certificate.
- The C(assertonly) provider is intended for use cases where one is only interested in - The C(assertonly) provider is intended for use cases where one is only interested in
checking properties of a supplied certificate. checking properties of a supplied certificate.
@ -58,9 +58,11 @@ options:
description: description:
- Name of the provider to use to generate/retrieve the OpenSSL certificate. - Name of the provider to use to generate/retrieve the OpenSSL certificate.
- The C(assertonly) provider will not generate files and fail if the certificate file is missing. - The C(assertonly) provider will not generate files and fail if the certificate file is missing.
- "The C(entrust) provider was added for Ansible 2.9 and requires credentials for the
L(https://www.entrustdatacard.com/products/categories/ssl-certificates,Entrust Certificate Services) (ECS) API."
type: str type: str
required: true required: true
choices: [ acme, assertonly, ownca, selfsigned ] choices: [ acme, assertonly, entrust, ownca, selfsigned ]
force: force:
description: description:
@ -298,7 +300,6 @@ options:
type: str type: str
aliases: [ notAfter ] aliases: [ notAfter ]
valid_in: valid_in:
description: description:
- The certificate must still be valid at this relative time offset from now. - The certificate must still be valid at this relative time offset from now.
@ -373,6 +374,97 @@ options:
default: no default: no
version_added: "2.8" version_added: "2.8"
entrust_cert_type:
description:
- The type of certificate product to request.
- This is only used by the C(entrust) provider.
type: str
default: STANDARD_SSL
choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
version_added: "2.9"
entrust_requester_email:
description:
- The email of the requester of the certificate (for tracking purposes).
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
version_added: "2.9"
entrust_requester_name:
description:
- The name of the requester of the certificate (for tracking purposes).
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
version_added: "2.9"
entrust_requester_phone:
description:
- The phone number of the requester of the certificate (for tracking purposes).
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
version_added: "2.9"
entrust_api_user:
description:
- The username for authentication to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
version_added: "2.9"
entrust_api_key:
description:
- The key (password) for authentication to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: str
version_added: "2.9"
entrust_api_client_cert_path:
description:
- The path of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: path
version_added: "2.9"
entrust_api_client_cert_key_path:
description:
- The path of the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
- This is only used by the C(entrust) provider.
- This is required if the provider is C(entrust).
type: path
version_added: "2.9"
entrust_not_after:
description:
- The point in time at which the certificate stops being valid.
- Time can be specified either as relative time or as absolute timestamp.
- Time will always be interpreted as UTC.
- Note that only the date (day, month, year) is supported for specifying expiry date of the issued certificate.
- The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day
earlier than expected if a relative time is used.
- The minimum certificate lifetime is 90 days, and maximum is three years.
- Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ C([w | d | h | m | s]) (e.g. C(+32w1d2h).
- If this value is not specified, the certificate will stop being valid 365 days from now.
- This is only used by the C(entrust) provider.
type: str
default: +365d
version_added: "2.9"
entrust_api_specification_path:
description:
- Path to the specification file defining the Entrust Certificate Services (ECS) API.
- Can be used to keep a local copy of the specification to avoid downloading it every time the module is used.
- This is only used by the C(entrust) provider.
type: path
default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
version_added: "2.9"
extends_documentation_fragment: files extends_documentation_fragment: files
notes: notes:
- All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern. - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
@ -421,6 +513,21 @@ EXAMPLES = r'''
acme_challenge_path: /etc/ssl/challenges/ansible.com/ acme_challenge_path: /etc/ssl/challenges/ansible.com/
force: yes force: yes
- name: Generate an Entrust certificate via the Entrust Certificate Services (ECS) API
openssl_certificate:
path: /etc/ssl/crt/ansible.com.crt
csr_path: /etc/ssl/csr/ansible.com.csr
provider: entrust
entrust_requester_name: Jo Doe
entrust_requester_email: jdoe@ansible.com
entrust_requester_phone: 555-555-5555
entrust_cert_type: STANDARD_SSL
entrust_api_user: apiusername
entrust_api_key: a^lv*32!cd9LnT
entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt
entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml
# Examples for some checks one could use the assertonly provider for: # Examples for some checks one could use the assertonly provider for:
# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow: # How to use the assertonly provider to implement and trigger your own custom certificate generation workflow:
@ -535,6 +642,7 @@ backup_file:
from random import randint from random import randint
import abc import abc
import datetime import datetime
import time
import os import os
import traceback import traceback
from distutils.version import LooseVersion from distutils.version import LooseVersion
@ -543,6 +651,7 @@ from ansible.module_utils import crypto as crypto_utils
from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native, to_bytes, to_text from ansible.module_utils._text import to_native, to_bytes, to_text
from ansible.module_utils.compat import ipaddress as compat_ipaddress from ansible.module_utils.compat import ipaddress as compat_ipaddress
from ansible.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_PYOPENSSL_VERSION = '0.15' MINIMAL_PYOPENSSL_VERSION = '0.15'
@ -564,7 +673,9 @@ try:
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.x509 import NameAttribute, Name from cryptography.x509 import NameAttribute, Name
from cryptography.x509.oid import NameOID
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError: except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@ -1701,6 +1812,167 @@ class AssertOnlyCertificate(AssertOnlyCertificateBase):
return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter() return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter()
class EntrustCertificate(Certificate):
"""Retrieve a certificate using Entrust (ECS)."""
def __init__(self, module, backend):
super(EntrustCertificate, self).__init__(module, backend)
self.trackingId = None
self.notAfter = self.get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after')
if not os.path.exists(self.csr_path):
raise CertificateError(
'The certificate signing request file {0} does not exist'.format(self.csr_path)
)
self.csr = crypto_utils.load_certificate_request(self.csr_path, backend=self.backend)
# ECS API defaults to using the validated organization tied to the account.
# We want to always force behavior of trying to use the organization provided in the CSR.
# To that end we need to parse out the organization from the CSR.
self.csr_org = None
if self.backend == 'pyopenssl':
csr_subject = self.csr.get_subject()
csr_subject_components = csr_subject.get_components()
for k, v in csr_subject_components:
if k.upper() == 'O':
# Entrust does not support multiple validated organizations in a single certificate
if self.csr_org is not None:
module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
"Subject DN: '{0}'. ".format(csr_subject)))
else:
self.csr_org = v
elif self.backend == 'cryptography':
csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
if len(csr_subject_orgs) == 1:
self.csr_org = csr_subject_orgs[0].value
elif len(csr_subject_orgs) > 1:
module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in "
"Subject DN: '{0}'. ".format(self.csr.subject)))
# If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to
# organization tied to the account.
if self.csr_org is None:
self.csr_org = ''
try:
self.ecs_client = ECSClient(
entrust_api_user=module.params.get('entrust_api_user'),
entrust_api_key=module.params.get('entrust_api_key'),
entrust_api_cert=module.params.get('entrust_api_client_cert_path'),
entrust_api_cert_key=module.params.get('entrust_api_client_cert_key_path'),
entrust_api_specification_path=module.params.get('entrust_api_specification_path')
)
except SessionConfigurationException as e:
module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message)))
def generate(self, module):
if not self.check(module, perms_required=False) or self.force:
# Read the CSR that was generated for us
body = {}
with open(self.csr_path, 'r') as csr_file:
body['csr'] = csr_file.read()
body['certType'] = module.params['entrust_cert_type']
# Handle expiration (30 days if not specified)
expiry = self.notAfter
if not expiry:
gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
expiry = gmt_now + datetime.timedelta(days=365)
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
body['certExpiryDate'] = expiry_iso3339
body['org'] = self.csr_org
body['tracking'] = {
'requesterName': module.params['entrust_requester_name'],
'requesterEmail': module.params['entrust_requester_email'],
'requesterPhone': module.params['entrust_requester_phone'],
}
try:
result = self.ecs_client.NewCertRequest(Body=body)
self.trackingId = result.get('trackingId')
except RestOperationException as e:
module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message)))
if self.backup:
self.backup_file = module.backup_local(self.path)
crypto_utils.write_file(module, to_bytes(result.get('endEntityCert')))
self.cert = crypto_utils.load_certificate(self.path, backend=self.backend)
self.changed = True
def check(self, module, perms_required=True):
"""Ensure the resource is in its desired state."""
parent_check = super(EntrustCertificate, self).check(module, perms_required)
try:
cert_details = self._get_cert_details()
except RestOperationException as e:
module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message)))
# Always issue a new certificate if the certificate is expired, suspended or revoked
status = cert_details.get('status', False)
if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED':
return False
# If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed
if module.params['entrust_cert_type'] and cert_details.get('certType') and module.params['entrust_cert_type'] != cert_details.get('certType'):
return False
return parent_check
def _get_cert_details(self):
cert_details = {}
if self.cert:
serial_number = None
expiry = None
if self.backend == 'pyopenssl':
serial_number = "{0:X}".format(self.cert.get_serial_number())
time_string = to_native(self.cert.get_notAfter())
expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ")
elif self.backend == 'cryptography':
serial_number = "{0:X}".format(self.cert.serial_number)
expiry = self.cert.not_valid_after
# get some information about the expiry of this certificate
expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
cert_details['expiresAfter'] = expiry_iso3339
# If a trackingId is not already defined (from the result of a generate)
# use the serial number to identify the tracking Id
if self.trackingId is None and serial_number is not None:
cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {})
# Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks
# on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is
# still checked as it is in the rest of the module.
if len(cert_results) == 1:
self.trackingId = cert_results[0].get('trackingId')
if self.trackingId is not None:
cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId))
return cert_details
def dump(self, check_mode=False):
result = {
'changed': self.changed,
'filename': self.path,
'privatekey': self.privatekey_path,
'csr': self.csr_path,
}
if self.backup_file:
result['backup_file'] = self.backup_file
result.update(self._get_cert_details())
return result
class AcmeCertificate(Certificate): class AcmeCertificate(Certificate):
"""Retrieve a certificate using the ACME protocol.""" """Retrieve a certificate using the ACME protocol."""
@ -1777,7 +2049,7 @@ def main():
argument_spec=dict( argument_spec=dict(
state=dict(type='str', default='present', choices=['present', 'absent']), state=dict(type='str', default='present', choices=['present', 'absent']),
path=dict(type='path', required=True), path=dict(type='path', required=True),
provider=dict(type='str', choices=['acme', 'assertonly', 'ownca', 'selfsigned']), provider=dict(type='str', choices=['acme', 'assertonly', 'entrust', 'ownca', 'selfsigned']),
force=dict(type='bool', default=False,), force=dict(type='bool', default=False,),
csr_path=dict(type='path'), csr_path=dict(type='path'),
backup=dict(type='bool', default=False), backup=dict(type='bool', default=False),
@ -1826,9 +2098,28 @@ def main():
acme_accountkey_path=dict(type='path'), acme_accountkey_path=dict(type='path'),
acme_challenge_path=dict(type='path'), acme_challenge_path=dict(type='path'),
acme_chain=dict(type='bool', default=False), acme_chain=dict(type='bool', default=False),
# provider: entrust
entrust_cert_type=dict(type='str', default='STANDARD_SSL',
choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL',
'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']),
entrust_requester_email=dict(type='str'),
entrust_requester_name=dict(type='str'),
entrust_requester_phone=dict(type='str'),
entrust_api_user=dict(type='str'),
entrust_api_key=dict(type='str', no_log=True),
entrust_api_client_cert_path=dict(type='path'),
entrust_api_client_cert_key_path=dict(type='path', no_log=True),
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'),
entrust_not_after=dict(type='str', default='+365d'),
), ),
supports_check_mode=True, supports_check_mode=True,
add_file_common_args=True, add_file_common_args=True,
required_if=[
['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone',
'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path',
'entrust_api_client_cert_key_path']]
]
) )
try: try:
@ -1887,6 +2178,8 @@ def main():
certificate = AcmeCertificate(module, 'pyopenssl') certificate = AcmeCertificate(module, 'pyopenssl')
elif provider == 'ownca': elif provider == 'ownca':
certificate = OwnCACertificate(module) certificate = OwnCACertificate(module)
elif provider == 'entrust':
certificate = EntrustCertificate(module, 'pyopenssl')
else: else:
certificate = AssertOnlyCertificate(module) certificate = AssertOnlyCertificate(module)
elif backend == 'cryptography': elif backend == 'cryptography':
@ -1902,6 +2195,8 @@ def main():
certificate = AcmeCertificate(module, 'cryptography') certificate = AcmeCertificate(module, 'cryptography')
elif provider == 'ownca': elif provider == 'ownca':
certificate = OwnCACertificateCryptography(module) certificate = OwnCACertificateCryptography(module)
elif provider == 'entrust':
certificate = EntrustCertificate(module, 'cryptography')
else: else:
certificate = AssertOnlyCertificateCryptography(module) certificate = AssertOnlyCertificateCryptography(module)